@intentius/chant-lexicon-terraform 0.124.52 → 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.map +1 -1
- package/dist/commands.js +4 -3
- 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 +15 -6
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +28 -55
- 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 +4 -3
- 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/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 +82 -1
- package/src/report.ts +31 -54
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +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,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraform-apply`'s group methods (chant#4447, ws-107): core forms a group
|
|
3
|
+
* of `terraform-apply` components that read each other, and these plan it
|
|
4
|
+
* as one plan and apply it as one apply of a composed root, so a value one
|
|
5
|
+
* root creates reaches its readers inside that apply.
|
|
6
|
+
*
|
|
7
|
+
* - `groupReads` reports to core each member's `terraform_remote_state` and
|
|
8
|
+
* `terraform_estate_outputs` reads of another, as the composition finds
|
|
9
|
+
* them, so roots wired in HCL alone form a group. Nothing while
|
|
10
|
+
* `terraform.compose.enabled` is unset.
|
|
11
|
+
* - `groupFallback` answers `off` while `terraform.compose.enabled` is unset,
|
|
12
|
+
* else builds the {@link Composition} (#4438) from the files, the config
|
|
13
|
+
* and each member's cloud identity, and answers its fallback, if any. A
|
|
14
|
+
* member the composition leaves out is dropped with its own kind and
|
|
15
|
+
* reason, and core forms the group again from the rest (#4419 ruling 1).
|
|
16
|
+
* A composition of estates also asks `choudoufu version` (#4445).
|
|
17
|
+
* Nothing plans.
|
|
18
|
+
* - `planGroup` builds the composition again, plans it once with
|
|
19
|
+
* `ComposedRun` (#4444) or, for estates, `EstateRun` (#4445), and answers
|
|
20
|
+
* each member's part (#4442): its change-set part, planned outputs and
|
|
21
|
+
* shape, with its own plan's digest. The group's digest is the composed
|
|
22
|
+
* plan's with the generated files. The run is released before it returns,
|
|
23
|
+
* with no lock taken, so a plan that core then refuses or makes wait
|
|
24
|
+
* leaves no lock held and no file changed.
|
|
25
|
+
* - `runGroup` plans once more, this time under each state's lock, and
|
|
26
|
+
* applies only when that plan's digest is the one `planGroup` answered;
|
|
27
|
+
* otherwise it throws a moved plan and nothing applies. Whatever happens,
|
|
28
|
+
* it releases the run. Each member's outputs come from the run: its part
|
|
29
|
+
* of the working state's `show -json` for roots with state, the post-apply
|
|
30
|
+
* plan's `prior_state` for estates.
|
|
31
|
+
*
|
|
32
|
+
* Each member's identity is `ci.identity.cloud` resolved for its root as
|
|
33
|
+
* the target (`resolveTargetCloudIdentity`, #4332), by the root's key or its
|
|
34
|
+
* directory, whichever a rule's target glob matches, compared by
|
|
35
|
+
* `identityKey`, so members that apply under different identities do not
|
|
36
|
+
* compose (#4438's `identity`).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
40
|
+
import { tmpdir } from "node:os";
|
|
41
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
42
|
+
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
43
|
+
import type {
|
|
44
|
+
CapabilityGroupOutcome,
|
|
45
|
+
CapabilityGroupPlan,
|
|
46
|
+
CapabilityPlan,
|
|
47
|
+
GroupContext,
|
|
48
|
+
GroupDrop,
|
|
49
|
+
GroupFallback,
|
|
50
|
+
GroupMember,
|
|
51
|
+
GroupMemberOutcome,
|
|
52
|
+
GroupRun,
|
|
53
|
+
ReportedGroupRead,
|
|
54
|
+
} from "@intentius/chant/components/capability";
|
|
55
|
+
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
56
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
57
|
+
import { wiringReferencesAt } from "@intentius/chant/components/driver";
|
|
58
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
59
|
+
import { declaredTargetCloudIdentity, type TargetCloudIdentityLookup } from "@intentius/chant/workspace/ci-cloud-identity";
|
|
60
|
+
import { COMPOSE_STEP_KIND, composeOff, compositionIfEnabled, identityKey, rootHclReads, type Composition } from "../compose";
|
|
61
|
+
import type { TerraformConfig } from "../config";
|
|
62
|
+
import { terraformBinary, terraformOutputs } from "../op/activities/terraform";
|
|
63
|
+
import { runBinary, type BinaryExec } from "../state/exec";
|
|
64
|
+
import { estateVersionFallback, EstateRun, type ComposedRunner } from "../compose/estate";
|
|
65
|
+
import { ComposedRun } from "../compose/run";
|
|
66
|
+
import { relPath, rootTexts } from "../compose/hcl";
|
|
67
|
+
|
|
68
|
+
/** The step input as the group methods read it; `TerraformApplyStepInput` in ./capability-plugin.ts. */
|
|
69
|
+
interface StepInput {
|
|
70
|
+
root: string;
|
|
71
|
+
vars?: Record<string, unknown>;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The project a group's roots belong to. */
|
|
76
|
+
export interface ComposeProject {
|
|
77
|
+
/** Where `chant.config.*` was found: roots resolve against it and `.chant-compose/` is written there. */
|
|
78
|
+
projectRoot: string;
|
|
79
|
+
terraform: TerraformConfig;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a runner is built from. */
|
|
83
|
+
export interface GroupRunnerOptions extends ComposeProject {
|
|
84
|
+
composition: Composition;
|
|
85
|
+
/** Take each state's lock: `runGroup` does, `planGroup` does not. */
|
|
86
|
+
lock: boolean;
|
|
87
|
+
/** Each member's step `vars` as written, by component. */
|
|
88
|
+
vars: Record<string, Record<string, unknown> | undefined>;
|
|
89
|
+
/** The outputs of components outside the group that a member's `vars` read. */
|
|
90
|
+
componentOutputs: Record<string, Record<string, unknown>>;
|
|
91
|
+
env: NodeJS.ProcessEnv;
|
|
92
|
+
/** A directory of the run's own, removed after the run is released. */
|
|
93
|
+
work: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What `planGroup` hands `runGroup` through `GroupRun.artifact`. */
|
|
97
|
+
interface ComposedArtifact {
|
|
98
|
+
/** The composed plan's digest `planGroup` answered (`CapabilityGroupPlan.digest`). */
|
|
99
|
+
digest: string;
|
|
100
|
+
estate: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What the group methods use, each replaceable in a test. */
|
|
104
|
+
export interface ComposeGroupDeps {
|
|
105
|
+
/** The project's config. Default: `loadChantConfigUpward` from the current directory. */
|
|
106
|
+
project?: () => Promise<ComposeProject>;
|
|
107
|
+
/** The run for a composition. Default: `EstateRun` when `estate` is set, else `ComposedRun`. */
|
|
108
|
+
runner?: (o: GroupRunnerOptions) => ComposedRunner;
|
|
109
|
+
/**
|
|
110
|
+
* Each target's cloud identity for `env` (`resolveTargetCloudIdentity`,
|
|
111
|
+
* #4332), or undefined when there is no workspace to read. Default:
|
|
112
|
+
* `declaredTargetCloudIdentity` from the project root.
|
|
113
|
+
*/
|
|
114
|
+
identity?: (projectRoot: string, env: string) => Promise<TargetCloudIdentityLookup | undefined>;
|
|
115
|
+
/** How `choudoufu version` runs. Default `runBinary`. */
|
|
116
|
+
exec?: BinaryExec;
|
|
117
|
+
/** A root's outputs as its state holds them, for a member whose part of the working state could not be shown. Default `terraformOutputs`. */
|
|
118
|
+
outputs?: (root: string) => Promise<Record<string, unknown>>;
|
|
119
|
+
env?: NodeJS.ProcessEnv;
|
|
120
|
+
log?: (line: string) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The group methods `terraform-apply` declares. */
|
|
124
|
+
export interface ComposeGroupMethods {
|
|
125
|
+
groupReads(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<ReportedGroupRead[]>;
|
|
126
|
+
groupFallback(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<GroupFallback | undefined>;
|
|
127
|
+
planGroup(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<CapabilityGroupPlan>;
|
|
128
|
+
runGroup(ctx: GroupContext, run: GroupRun, members: readonly GroupMember<StepInput>[]): Promise<CapabilityGroupOutcome>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadProject(): Promise<ComposeProject> {
|
|
132
|
+
const start = resolve(process.cwd());
|
|
133
|
+
const { config, configPath } = await loadChantConfigUpward(start);
|
|
134
|
+
const projectRoot = configPath ? dirname(configPath) : start;
|
|
135
|
+
const terraform = (config as { terraform?: TerraformConfig }).terraform;
|
|
136
|
+
return { projectRoot, terraform: { ...(terraform ?? {}), roots: terraform?.roots ?? {} } as TerraformConfig };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function defaultRunner(o: GroupRunnerOptions, log?: (line: string) => void): ComposedRunner {
|
|
140
|
+
if (o.composition.estate) {
|
|
141
|
+
return new EstateRun({ projectRoot: o.projectRoot, composition: o.composition, terraform: o.terraform, env: o.env, work: o.work, ...(log ? { log } : {}) });
|
|
142
|
+
}
|
|
143
|
+
return new ComposedRun({
|
|
144
|
+
projectRoot: o.projectRoot,
|
|
145
|
+
composition: o.composition,
|
|
146
|
+
terraform: o.terraform,
|
|
147
|
+
binary: { path: terraformBinary(o.terraform) },
|
|
148
|
+
env: o.env,
|
|
149
|
+
work: o.work,
|
|
150
|
+
lock: o.lock,
|
|
151
|
+
...(o.env.GITHUB_RUN_ID ? { runId: `github-run-${o.env.GITHUB_RUN_ID}` } : {}),
|
|
152
|
+
vars: o.vars,
|
|
153
|
+
componentOutputs: o.componentOutputs,
|
|
154
|
+
// A state that could not be written is kept here when its backend cannot take it; it outlives the run's own directory.
|
|
155
|
+
reportDir: join(o.projectRoot, ".chant", "compose", "unwritten"),
|
|
156
|
+
...(log ? { log } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The members as components, each deploying its one step, for `compositionIfEnabled`. */
|
|
161
|
+
export function memberComponents(members: readonly GroupMember<StepInput>[]): DriverComponent[] {
|
|
162
|
+
return members.map((m) => ({
|
|
163
|
+
name: m.component,
|
|
164
|
+
...(m.dependsOn ? { dependsOn: [...m.dependsOn] } : {}),
|
|
165
|
+
deploy: [{ phase: "Apply", steps: [{ ...m.step, kind: COMPOSE_STEP_KIND }] }],
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Each member's identity as `compositionFor` compares it: `identityKey` of
|
|
171
|
+
* `lookup` for the member's root, which a rule's target globs match by the
|
|
172
|
+
* root's key in `terraform.roots` or by its directory relative to the
|
|
173
|
+
* project (the first declared rule that either matches wins). Undefined
|
|
174
|
+
* without a lookup, so no member falls back as `identity`.
|
|
175
|
+
*/
|
|
176
|
+
export function memberIdentity(
|
|
177
|
+
project: ComposeProject,
|
|
178
|
+
members: readonly GroupMember<StepInput>[],
|
|
179
|
+
lookup: TargetCloudIdentityLookup | undefined,
|
|
180
|
+
): ((member: string) => string | undefined) | undefined {
|
|
181
|
+
if (!lookup) return undefined;
|
|
182
|
+
const names = (root: string): string[] => {
|
|
183
|
+
const dir = project.terraform.roots[root]?.dir;
|
|
184
|
+
if (dir === undefined) return [root];
|
|
185
|
+
return [root, relPath(project.projectRoot, isAbsolute(dir) ? dir : resolve(project.projectRoot, dir))];
|
|
186
|
+
};
|
|
187
|
+
const keys = new Map(members.map((m) => [m.component, identityKey(lookup(names(m.step.root as string)))] as const));
|
|
188
|
+
return (member) => keys.get(member);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The composition of exactly the group's members (core has already formed the
|
|
193
|
+
* group), or undefined when `terraform.compose` is not enabled.
|
|
194
|
+
*/
|
|
195
|
+
export function groupComposition(
|
|
196
|
+
project: ComposeProject,
|
|
197
|
+
members: readonly GroupMember<StepInput>[],
|
|
198
|
+
env: NodeJS.ProcessEnv,
|
|
199
|
+
identity?: (member: string) => string | undefined,
|
|
200
|
+
): Composition | undefined {
|
|
201
|
+
const targets = Object.fromEntries(members.flatMap((m) => (m.ctx.targets?.length ? [[m.component, m.ctx.targets] as const] : [])));
|
|
202
|
+
return compositionIfEnabled({
|
|
203
|
+
projectRoot: project.projectRoot,
|
|
204
|
+
terraform: project.terraform,
|
|
205
|
+
components: memberComponents(members),
|
|
206
|
+
selected: new Set(members.map((m) => m.component)),
|
|
207
|
+
env,
|
|
208
|
+
targets,
|
|
209
|
+
...(identity ? { identity } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
214
|
+
|
|
215
|
+
/** The data source and the attribute that holds its outputs, for each kind of HCL read. */
|
|
216
|
+
const HCL_READ = { "remote-state": ["terraform_remote_state", "outputs"], "estate-outputs": ["terraform_estate_outputs", "values"] } as const;
|
|
217
|
+
|
|
218
|
+
/** The outputs the texts read off `data.<type>.<name>.<field>`, by attribute or by index, sorted. */
|
|
219
|
+
function outputsRead(texts: readonly string[], type: string, name: string, field: string): string[] {
|
|
220
|
+
const re = new RegExp(`\\bdata\\.${type}\\.${escapeRe(name)}\\.${field}(?:\\.([A-Za-z_][\\w-]*)|\\[\\s*\\\\?"([^"\\\\]+)\\\\?"\\s*\\])`, "g");
|
|
221
|
+
const out = new Set<string>();
|
|
222
|
+
for (const text of texts) for (const m of text.matchAll(re)) out.add((m[1] ?? m[2])!);
|
|
223
|
+
return [...out].sort();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The members' HCL reads of each other (#4447): each `terraform_remote_state`
|
|
228
|
+
* block whose state is another member's backend, and each
|
|
229
|
+
* `terraform_estate_outputs` block naming another member's estate, as the
|
|
230
|
+
* composition finds them. Each output the reader's code takes off the block
|
|
231
|
+
* is one read, `<upstream>.<output>` as a `stackOutput()` read is named; a
|
|
232
|
+
* block whose outputs the code does not name one by one is one read with
|
|
233
|
+
* the block's address as `ref`. Core cannot see these in the step's `vars`.
|
|
234
|
+
*/
|
|
235
|
+
export function hclGroupReads(project: ComposeProject, members: readonly GroupMember<StepInput>[]): ReportedGroupRead[] {
|
|
236
|
+
const reads = rootHclReads({ projectRoot: project.projectRoot, terraform: project.terraform, components: memberComponents(members) });
|
|
237
|
+
const out: ReportedGroupRead[] = [];
|
|
238
|
+
for (const [reader, rs] of reads) {
|
|
239
|
+
const m = members.find((x) => x.component === reader);
|
|
240
|
+
const dir = m ? project.terraform.roots[m.step.root as string]?.dir : undefined;
|
|
241
|
+
const texts = dir === undefined ? [] : rootTexts(isAbsolute(dir) ? dir : resolve(project.projectRoot, dir), project.terraform.binary).map((t) => t.text);
|
|
242
|
+
for (const r of rs) {
|
|
243
|
+
if (r.via === "vars") continue;
|
|
244
|
+
const [type, field] = HCL_READ[r.via];
|
|
245
|
+
const named = outputsRead(texts, type, r.name, field);
|
|
246
|
+
if (named.length === 0) out.push({ reader, upstream: r.upstream, ref: `data.${type}.${r.name}` });
|
|
247
|
+
for (const output of named) out.push({ reader, upstream: r.upstream, ref: `${r.upstream}.${output}`, output });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* What core is told of the group (#4419 ruling 1): the whole change's
|
|
255
|
+
* fallback when it is not `one-root`; else each member the composition left
|
|
256
|
+
* out as a drop with its own kind and reason, so core groups the rest
|
|
257
|
+
* again without them; else `one-root`. Undefined when every member
|
|
258
|
+
* composes.
|
|
259
|
+
*/
|
|
260
|
+
export function groupFallbackOf(c: Composition, members: readonly string[]): GroupFallback | undefined {
|
|
261
|
+
if (c.fallback && c.fallback.kind !== "one-root") return { kind: c.fallback.kind, reason: c.fallback.reason };
|
|
262
|
+
const drop: GroupDrop[] = c.fallbacks.flatMap((f) => (f.member !== undefined && members.includes(f.member) ? [{ component: f.member, kind: f.kind, reason: f.reason }] : []));
|
|
263
|
+
if (!c.fallback) {
|
|
264
|
+
const named = new Set(drop.map((d) => d.component));
|
|
265
|
+
for (const m of members) {
|
|
266
|
+
if (!named.has(m) && !c.members.includes(m)) drop.push({ component: m, kind: "path", reason: `\`${m}\` deploys no root the composition could read` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (drop.length > 0) return { kind: drop[0]!.kind!, reason: drop[0]!.reason!, drop };
|
|
270
|
+
if (c.fallback) return { kind: c.fallback.kind, reason: c.fallback.reason };
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** `value` at the JSON Pointer `at`. */
|
|
275
|
+
function atPointer(value: unknown, at: string): unknown {
|
|
276
|
+
if (at === "") return value;
|
|
277
|
+
return at
|
|
278
|
+
.slice(1)
|
|
279
|
+
.split("/")
|
|
280
|
+
.map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
281
|
+
.reduce<unknown>((acc, key) => (acc == null ? acc : (acc as Record<string, unknown>)[key]), value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Set `value` at the dotted `path` of `target`. */
|
|
285
|
+
function setPath(target: Record<string, unknown>, path: string, value: unknown): void {
|
|
286
|
+
const keys = path.split(".");
|
|
287
|
+
let at = target;
|
|
288
|
+
for (const k of keys.slice(0, -1)) {
|
|
289
|
+
const next = at[k];
|
|
290
|
+
at = (next && typeof next === "object" ? next : (at[k] = {})) as Record<string, unknown>;
|
|
291
|
+
}
|
|
292
|
+
at[keys[keys.length - 1]!] = value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The outputs of components outside the group that the members' `vars`
|
|
297
|
+
* read, recovered from each member's input, which core resolved against
|
|
298
|
+
* them: `writeComposedRoot` turns those reads into literals.
|
|
299
|
+
*/
|
|
300
|
+
export function outsideOutputs(members: readonly GroupMember<StepInput>[]): Record<string, Record<string, unknown>> {
|
|
301
|
+
const inGroup = new Set(members.map((m) => m.component));
|
|
302
|
+
const out: Record<string, Record<string, unknown>> = {};
|
|
303
|
+
for (const m of members) {
|
|
304
|
+
for (const r of wiringReferencesAt(m.step)) {
|
|
305
|
+
if (inGroup.has(r.source)) continue;
|
|
306
|
+
const value = atPointer(m.input, r.at);
|
|
307
|
+
if (value !== undefined) setPath((out[r.source] ??= {}), r.output, value);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The members' unresolved reads of components outside the group, by member. */
|
|
314
|
+
function unresolvedOf(members: readonly GroupMember<StepInput>[]): Map<string, string[]> {
|
|
315
|
+
return new Map(members.flatMap((m) => (m.ctx.unresolvedInputs?.length ? [[m.component, [...m.ctx.unresolvedInputs].sort()] as const] : [])));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The group while a member reads an output of a component outside it that
|
|
320
|
+
* is not known yet: no member is planned, each gets a hole naming what the
|
|
321
|
+
* group waits on, and core plans the group again once those apply.
|
|
322
|
+
*/
|
|
323
|
+
function waitingGroup(ctx: GroupContext, members: readonly GroupMember<StepInput>[], unresolved: ReadonlyMap<string, string[]>): CapabilityGroupPlan {
|
|
324
|
+
const waits = [...new Set([...unresolved.values()].flat())].sort();
|
|
325
|
+
const by = [...unresolved.keys()].sort();
|
|
326
|
+
const plans: Record<string, CapabilityPlan> = {};
|
|
327
|
+
for (const m of members) {
|
|
328
|
+
const root = m.step.root as string;
|
|
329
|
+
const vars = Object.fromEntries(Object.entries((m.input?.vars ?? {}) as Record<string, unknown>).map(([k, v]) => [k, v === undefined ? null : v]));
|
|
330
|
+
const planDigest = computePlanDigest("terraform-apply-group-unresolved", { env: ctx.env, root, vars, unresolved: waits });
|
|
331
|
+
const reason =
|
|
332
|
+
by.length === 1 && by[0] === m.component
|
|
333
|
+
? `inputs from ${waits.join(", ")}; planned again once they apply`
|
|
334
|
+
: `inputs from ${waits.join(", ")}, read by ${by.join(", ")} in its group; planned again once they apply`;
|
|
335
|
+
const changeSet: ChangeSetPart = {
|
|
336
|
+
member: { member: root, lexicon: "terraform", planner: "chant", status: "planned", planDigest, holes: [{ address: root, type: "terraform_root", reason }] },
|
|
337
|
+
entries: [],
|
|
338
|
+
};
|
|
339
|
+
plans[m.component] = { member: root, planDigest, changeSet };
|
|
340
|
+
}
|
|
341
|
+
return { members: plans, digest: computePlanDigest("terraform-apply-group-unresolved", { components: ctx.components, unresolved: waits }) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A composed plan that no longer matches the one approved. Core's moved-plan mark, so nothing applies and the command exits 3. */
|
|
345
|
+
export class ComposedPlanMovedError extends Error {
|
|
346
|
+
readonly planMoved = true;
|
|
347
|
+
constructor(message: string) {
|
|
348
|
+
super(message);
|
|
349
|
+
this.name = "ComposedPlanMovedError";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The group methods, with `deps` in place of the defaults. */
|
|
354
|
+
export function composeGroup(deps: ComposeGroupDeps = {}): ComposeGroupMethods {
|
|
355
|
+
const project = deps.project ?? loadProject;
|
|
356
|
+
const exec = deps.exec ?? runBinary;
|
|
357
|
+
const envOf = (): NodeJS.ProcessEnv => deps.env ?? process.env;
|
|
358
|
+
const log = deps.log ?? ((line: string) => console.error(line));
|
|
359
|
+
const readOutputs = deps.outputs ?? ((root: string) => terraformOutputs({ root }));
|
|
360
|
+
const identities = deps.identity ?? declaredTargetCloudIdentity;
|
|
361
|
+
|
|
362
|
+
/** The composition of `members` as `groupFallback` judges it, with each member's identity. */
|
|
363
|
+
async function compositionOf(ctx: GroupContext, p: ComposeProject, members: readonly GroupMember<StepInput>[]): Promise<Composition> {
|
|
364
|
+
const lookup = await identities(p.projectRoot, ctx.env);
|
|
365
|
+
const c = groupComposition(p, members, envOf(), memberIdentity(p, members, lookup));
|
|
366
|
+
if (!c) throw new Error(composeOff(p.terraform)?.reason ?? "`terraform.compose` is not enabled");
|
|
367
|
+
return c;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The composition the group plans as, or throws when it does not compose. */
|
|
371
|
+
async function composed(ctx: GroupContext, members: readonly GroupMember<StepInput>[]): Promise<{ p: ComposeProject; c: Composition }> {
|
|
372
|
+
const p = await project();
|
|
373
|
+
const off = composeOff(p.terraform);
|
|
374
|
+
if (off) throw new Error(off.reason);
|
|
375
|
+
const c = await compositionOf(ctx, p, members);
|
|
376
|
+
const refused = groupFallbackOf(c, members.map((m) => m.component));
|
|
377
|
+
if (refused) throw new Error(`the group no longer composes: ${refused.reason}`);
|
|
378
|
+
return { p, c };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Build a runner, run `fn` on it, and release it and its directory whatever happened. */
|
|
382
|
+
async function withRunner<T>(p: ComposeProject, c: Composition, members: readonly GroupMember<StepInput>[], lock: boolean, fn: (r: ComposedRunner) => Promise<T>): Promise<T> {
|
|
383
|
+
const work = mkdtempSync(join(tmpdir(), "chant-compose-"));
|
|
384
|
+
const o: GroupRunnerOptions = {
|
|
385
|
+
...p,
|
|
386
|
+
composition: c,
|
|
387
|
+
lock,
|
|
388
|
+
vars: Object.fromEntries(members.map((m) => [m.component, m.step.vars as Record<string, unknown> | undefined])),
|
|
389
|
+
componentOutputs: outsideOutputs(members),
|
|
390
|
+
env: envOf(),
|
|
391
|
+
work,
|
|
392
|
+
};
|
|
393
|
+
const runner = deps.runner ? deps.runner(o) : defaultRunner(o, log);
|
|
394
|
+
try {
|
|
395
|
+
return await fn(runner);
|
|
396
|
+
} finally {
|
|
397
|
+
try {
|
|
398
|
+
await runner.release();
|
|
399
|
+
} finally {
|
|
400
|
+
rmSync(work, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
async groupReads(_ctx, members) {
|
|
407
|
+
const p = await project();
|
|
408
|
+
if (composeOff(p.terraform)) return [];
|
|
409
|
+
return hclGroupReads(p, members);
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
async groupFallback(ctx, members) {
|
|
413
|
+
const p = await project();
|
|
414
|
+
const off = composeOff(p.terraform);
|
|
415
|
+
if (off) return { kind: off.kind, reason: off.reason };
|
|
416
|
+
const c = await compositionOf(ctx, p, members);
|
|
417
|
+
// A member left out is dropped alone; core asks again with the rest, and the estate check runs then.
|
|
418
|
+
const refused = groupFallbackOf(c, members.map((m) => m.component));
|
|
419
|
+
if (refused) return refused;
|
|
420
|
+
if (c.estate) {
|
|
421
|
+
const binary = p.terraform.binary ?? "choudoufu";
|
|
422
|
+
const v = await exec(binary, ["version"], p.projectRoot, envOf());
|
|
423
|
+
// A binary that cannot say its version is left to the plan, which says why it fails.
|
|
424
|
+
const old = v.code === 0 ? estateVersionFallback(v.stdout) : undefined;
|
|
425
|
+
if (old) return { kind: old.kind, reason: old.reason };
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
async planGroup(ctx, members) {
|
|
431
|
+
const unresolved = unresolvedOf(members);
|
|
432
|
+
if (unresolved.size > 0) return waitingGroup(ctx, members, unresolved);
|
|
433
|
+
const { p, c } = await composed(ctx, members);
|
|
434
|
+
// Planned without locks and released at once: nothing stays held or changed if core refuses the plan or makes it wait.
|
|
435
|
+
const planned = await withRunner(p, c, members, false, (r) => r.plan());
|
|
436
|
+
if (planned.fallback) throw new Error(`the group no longer composes: ${planned.fallback.reason}`);
|
|
437
|
+
if (planned.error) throw new Error(planned.error);
|
|
438
|
+
const plans: Record<string, CapabilityPlan> = {};
|
|
439
|
+
for (const m of members) {
|
|
440
|
+
const part = planned.members[m.component];
|
|
441
|
+
if (!part) throw new Error(`the composed plan has no part for ${m.component}`);
|
|
442
|
+
plans[m.component] = { member: part.member, planDigest: part.planDigest, changeSet: part.changeSet, outputs: part.outputs, shape: part.shape };
|
|
443
|
+
}
|
|
444
|
+
const artifact: ComposedArtifact = { digest: planned.digest, estate: c.estate === true };
|
|
445
|
+
return { members: plans, digest: planned.digest, artifact };
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
async runGroup(ctx, run, members) {
|
|
449
|
+
if (unresolvedOf(members).size > 0) throw new Error("the group reads outputs of components outside it that are not known yet, so it cannot apply");
|
|
450
|
+
const approved = run.artifact as ComposedArtifact | undefined;
|
|
451
|
+
if (typeof approved?.digest !== "string" || approved.digest === "") throw new Error("no composed plan of the group to apply");
|
|
452
|
+
const { p, c } = await composed(ctx, members);
|
|
453
|
+
const notStarted = (error: string): CapabilityGroupOutcome => ({
|
|
454
|
+
members: Object.fromEntries(members.map((m) => [m.component, { status: "not-started" as const, error }])),
|
|
455
|
+
error,
|
|
456
|
+
});
|
|
457
|
+
const applied = await withRunner(p, c, members, true, async (r) => {
|
|
458
|
+
// The approved plan was made without locks; plan again under them, and apply only that same plan.
|
|
459
|
+
const planned = await r.plan();
|
|
460
|
+
if (planned.fallback) return notStarted(`the group no longer composes: ${planned.fallback.reason}`);
|
|
461
|
+
if (planned.error) return notStarted(planned.error);
|
|
462
|
+
if (planned.digest !== approved.digest) {
|
|
463
|
+
throw new ComposedPlanMovedError(`the composed plan moved since it was approved (approved ${approved.digest}, planned now ${planned.digest}), so nothing applied`);
|
|
464
|
+
}
|
|
465
|
+
return r.apply();
|
|
466
|
+
});
|
|
467
|
+
const out: Record<string, GroupMemberOutcome> = {};
|
|
468
|
+
for (const m of members) {
|
|
469
|
+
const o = applied.members[m.component] as (GroupMemberOutcome & { lockHeld?: true; kept?: string }) | undefined;
|
|
470
|
+
if (!o) {
|
|
471
|
+
out[m.component] = { status: "failed", error: applied.error ?? `the composed apply said nothing of ${m.component}` };
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
let outputs = o.outputs;
|
|
475
|
+
// A root's state is written by now; read its outputs from there when its part of the working state could not be shown.
|
|
476
|
+
if (!outputs && !c.estate && (o.status === "applied" || o.status === "no-changes")) {
|
|
477
|
+
outputs = await readOutputs(m.step.root as string).catch(() => undefined);
|
|
478
|
+
}
|
|
479
|
+
const held = o.lockHeld ? `its state was kept unwritten${o.kept ? ` at ${o.kept}` : ""}, and its lock stays held until a later run writes it` : undefined;
|
|
480
|
+
const error = [o.error, held].filter(Boolean).join("; ");
|
|
481
|
+
out[m.component] = { status: o.status, ...(outputs ? { outputs } : {}), ...(error ? { error } : {}) };
|
|
482
|
+
}
|
|
483
|
+
return { members: out, ...(applied.error ? { error: applied.error } : {}) };
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The group methods `terraform-apply` uses. */
|
|
489
|
+
export const terraformComposeGroup: ComposeGroupMethods = composeGroup();
|