@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
|
@@ -120,6 +120,14 @@
|
|
|
120
120
|
* not-observed, then foreign, then adoptable, then owned, and a block whose
|
|
121
121
|
* instances are all absent is absent. Partial knowledge is not knowledge,
|
|
122
122
|
* which is the same rule choudoufu applies to its own `INCOMPLETE_BLOCK`.
|
|
123
|
+
* The precedence is core's `TERRAFORM_INSTANCE_ROLLUP` and the rollup is
|
|
124
|
+
* core's `rollupInstances` (`@intentius/chant/instance-nodes`, #4454), the
|
|
125
|
+
* one place it is written.
|
|
126
|
+
*
|
|
127
|
+
* The block's verdict is not the whole answer. A present block also carries
|
|
128
|
+
* each instance's own verdict on `instances`, keyed as Terraform prints it
|
|
129
|
+
* (`[0]`, `["a"]`), which the live graph draws as an instance node under the
|
|
130
|
+
* block node (#4454). The state read does the same with the rows it found.
|
|
123
131
|
*
|
|
124
132
|
* ## What is readable at all
|
|
125
133
|
*
|
|
@@ -195,6 +203,31 @@
|
|
|
195
203
|
* The cost is two refreshes of the estate per read, run one after the other
|
|
196
204
|
* so they never share the root's cache file.
|
|
197
205
|
*
|
|
206
|
+
* ## Engine facts on a live root (#4528)
|
|
207
|
+
*
|
|
208
|
+
* After `live-plan -json` answers, the same document is handed to `choudoufu
|
|
209
|
+
* live-graph -json -from-plan=<file>` (choudoufu#2285), which builds the
|
|
210
|
+
* root's graph from the configuration and that file alone: no second live
|
|
211
|
+
* read, so the facts and the verdicts above come from one refresh (choudoufu
|
|
212
|
+
* #1014, the #4520 ruling Q3). `<file>` is the `<root>.live-plan.json`
|
|
213
|
+
* written under {@link TERRAFORM_LIVE_PLAN_DIR_ENV} when the caller asked
|
|
214
|
+
* for it, and otherwise a copy in a temporary directory removed afterwards.
|
|
215
|
+
*
|
|
216
|
+
* The document's node ids are choudoufu's addresses (the ruling's Q2). A
|
|
217
|
+
* block node maps to its entity with {@link entityKeyFor}; an instance node
|
|
218
|
+
* (`instanceOf` set) maps to its block's entity, and its facts go on that
|
|
219
|
+
* block's `instances` entry of the same address, from which core's
|
|
220
|
+
* `observedInstanceNodeId` draws the instance node. Each node's `facts` land
|
|
221
|
+
* on `ResourceMetadata.facts` or `InstanceObservation.facts`, and edges with
|
|
222
|
+
* a grain land on the observation's `edges`, endpoints mapped the same way
|
|
223
|
+
* ({@link readEngineGraph}). Facts are never read for a decision: ownership
|
|
224
|
+
* and the change set keep reading the verdicts above.
|
|
225
|
+
*
|
|
226
|
+
* A choudoufu without the command, older than
|
|
227
|
+
* `MIN_CHOUDOUFU_GRAPH_VERSION`, answers with a usage error: `facts` stay
|
|
228
|
+
* absent, the root gets one note, and the rest of the read is unchanged. Any
|
|
229
|
+
* other failure of the graph read degrades the same way, with its own note.
|
|
230
|
+
*
|
|
198
231
|
* ## Nothing from `values` is surfaced
|
|
199
232
|
*
|
|
200
233
|
* A state row's `values` are the resource's full attribute set, secrets
|
|
@@ -223,11 +256,23 @@ import {
|
|
|
223
256
|
type UnobservedReason,
|
|
224
257
|
} from "@intentius/chant/observation";
|
|
225
258
|
import type { OwnershipMarker } from "@intentius/chant/ownership";
|
|
226
|
-
import {
|
|
259
|
+
import {
|
|
260
|
+
observedInstanceNodeId,
|
|
261
|
+
rollupInstances,
|
|
262
|
+
terraformInstancePath,
|
|
263
|
+
TERRAFORM_INSTANCE_ROLLUP,
|
|
264
|
+
type InstanceObservation,
|
|
265
|
+
} from "@intentius/chant/instance-nodes";
|
|
266
|
+
import type { EdgeGrain, EngineFacts } from "@intentius/chant/engine-facts";
|
|
267
|
+
import type { IREdge } from "@intentius/chant/graph-ir";
|
|
268
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
269
|
+
import { tmpdir } from "node:os";
|
|
227
270
|
import { isAbsolute, join } from "node:path";
|
|
228
271
|
import {
|
|
272
|
+
choudoufuLiveGraph,
|
|
229
273
|
choudoufuLiveLs,
|
|
230
274
|
choudoufuLivePlanDocument,
|
|
275
|
+
MIN_CHOUDOUFU_GRAPH_VERSION,
|
|
231
276
|
terraformInit,
|
|
232
277
|
terraformPlanRead,
|
|
233
278
|
terraformShow,
|
|
@@ -235,6 +280,7 @@ import {
|
|
|
235
280
|
} from "./op/activities/terraform";
|
|
236
281
|
import { blockAddressOf, indexPlan, planAttributesFor, plannedAttributesFor, type PlanIndex } from "./plan-observation";
|
|
237
282
|
import type { TerraformObserveDepth } from "./config";
|
|
283
|
+
import { LIVE_PLAN_OMISSION_REASONS, readsAsCredentials } from "./live-plan-reasons";
|
|
238
284
|
import { DATA_TYPE, MODULE_TYPE, RESOURCE_TYPE, callersOfKey } from "./hcl/parse";
|
|
239
285
|
|
|
240
286
|
// The channel keys live in their own module so `plugin.ts` can declare
|
|
@@ -242,6 +288,7 @@ import { DATA_TYPE, MODULE_TYPE, RESOURCE_TYPE, callersOfKey } from "./hcl/parse
|
|
|
242
288
|
// this is where they are used.
|
|
243
289
|
export { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
|
|
244
290
|
export { TERRAFORM_LIVE_MARKER_KEYS } from "./live-ownership";
|
|
291
|
+
export { LIVE_PLAN_OMISSION_REASONS } from "./live-plan-reasons";
|
|
245
292
|
|
|
246
293
|
/** One row out of `values.root_module.resources[]`, at any module depth. */
|
|
247
294
|
export interface StateResourceRow {
|
|
@@ -329,10 +376,24 @@ export function classifyStateOwnership(address: string, index: StateIndex): "own
|
|
|
329
376
|
return index.rows.has(address) ? "owned" : "unknown";
|
|
330
377
|
}
|
|
331
378
|
|
|
379
|
+
/**
|
|
380
|
+
* True when `instance` is the module call `address` itself or sits beneath it,
|
|
381
|
+
* at any instance key. `address` is a declared module address with no keys
|
|
382
|
+
* (`module.app`, `module.outer.module.inner`); a state or live-plan address
|
|
383
|
+
* carries them (`module.app[0]`, `module.outer["a"].module.inner[1].aws_eip.x`),
|
|
384
|
+
* so the keys come off before the prefix test (#4513).
|
|
385
|
+
*/
|
|
386
|
+
function underModule(instance: string, address: string): boolean {
|
|
387
|
+
const block = blockAddressOf(instance);
|
|
388
|
+
return block === address || block.startsWith(`${address}.`);
|
|
389
|
+
}
|
|
390
|
+
|
|
332
391
|
/** True when the state carries at least one resource beneath `module.<name>`. */
|
|
333
392
|
function moduleIsLive(address: string, index: StateIndex): boolean {
|
|
334
|
-
|
|
335
|
-
|
|
393
|
+
// A keyed call's child_modules[] entries are `module.app[0]`, never
|
|
394
|
+
// `module.app`, and so are its rows' prefixes (#4513).
|
|
395
|
+
for (const module of index.modules) if (underModule(module, address)) return true;
|
|
396
|
+
for (const key of index.rows.keys()) if (underModule(key, address)) return true;
|
|
336
397
|
return false;
|
|
337
398
|
}
|
|
338
399
|
|
|
@@ -500,43 +561,6 @@ export function indexLivePlan(document: unknown): LivePlanIndex {
|
|
|
500
561
|
return index;
|
|
501
562
|
}
|
|
502
563
|
|
|
503
|
-
/**
|
|
504
|
-
* choudoufu's omission reason codes, mapped onto chant's total vocabulary
|
|
505
|
-
* (`internal/live/projection/result.go`). `"absent"` is not an
|
|
506
|
-
* {@link UnobservedReason}: it is the tri-state's OBSERVED-ABSENT, the one
|
|
507
|
-
* omission that is a claim of knowledge rather than of ignorance.
|
|
508
|
-
*
|
|
509
|
-
* A code this table does not know maps to `read-failed`, which is the safe
|
|
510
|
-
* direction: no NOT-OBSERVED verdict ever becomes a create or a delete.
|
|
511
|
-
*/
|
|
512
|
-
export const LIVE_PLAN_OMISSION_REASONS: Readonly<Record<string, UnobservedReason | "absent">> = {
|
|
513
|
-
// The provider was asked and reported, normally, that nothing is there.
|
|
514
|
-
ABSENT: "absent",
|
|
515
|
-
// The provider errored, or could not be reached in a usable order.
|
|
516
|
-
FAILED: "read-failed",
|
|
517
|
-
PARENT_UNAVAILABLE: "read-failed",
|
|
518
|
-
CYCLE: "read-failed",
|
|
519
|
-
INCOMPLETE_BLOCK: "read-failed",
|
|
520
|
-
LISTED_NOT_IMPORTABLE: "read-failed",
|
|
521
|
-
// Something exists at the identity and does not carry this estate's marker.
|
|
522
|
-
// Normally answered by the `unowned[]` section before this table is read;
|
|
523
|
-
// this entry is what happens if a document ever carries the omission alone.
|
|
524
|
-
UNOWNED: "read-failed",
|
|
525
|
-
// The mode cannot name, list or tag this instance at all: a server-assigned
|
|
526
|
-
// identity with no marker to find it by, a resource with no cloud object to
|
|
527
|
-
// read, or an address another declared instance has taken over.
|
|
528
|
-
NEEDS_DISCOVERY: "unsupported-kind",
|
|
529
|
-
UNREADABLE: "unsupported-kind",
|
|
530
|
-
SUPERSEDED: "unsupported-kind",
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
/** Does this text name a credentials or authorization failure rather than any other error? */
|
|
534
|
-
function readsAsCredentials(text: string): boolean {
|
|
535
|
-
return /\b(no valid credential|credentials?|not authorized|unauthorized|accessdenied|access denied|expiredtoken|invalidclienttokenid|signature)\b/i.test(
|
|
536
|
-
text,
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
564
|
/** The chant verdict for one omission: `no-credentials` when its detail says so. */
|
|
541
565
|
function omissionVerdict(row: LivePlanOmissionRow): UnobservedReason | "absent" {
|
|
542
566
|
const mapped = LIVE_PLAN_OMISSION_REASONS[row.reason] ?? "read-failed";
|
|
@@ -550,14 +574,16 @@ function omissionVerdict(row: LivePlanOmissionRow): UnobservedReason | "absent"
|
|
|
550
574
|
* #2360), which places the same blocks against the same document.
|
|
551
575
|
*/
|
|
552
576
|
export function instancesOf(address: string, index: LivePlanIndex): string[] {
|
|
553
|
-
|
|
577
|
+
// `blockAddressOf` strips every key, so an instance under an expanded
|
|
578
|
+
// module call (`module.app[0].aws_eip.pool[1]`) is the block's too (#4488).
|
|
579
|
+
return index.addresses.filter((a) => a === address || a.startsWith(`${address}[`) || blockAddressOf(a) === address);
|
|
554
580
|
}
|
|
555
581
|
|
|
556
582
|
/** True when the document names any instance under `module.<name>`, at any depth. */
|
|
557
583
|
function liveModuleMembers(address: string, index: LivePlanIndex): string[] {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
);
|
|
584
|
+
// Keys stripped first, so a nested call under a keyed one
|
|
585
|
+
// (`module.outer[0].module.inner["a"].…`) is a member too (#4513).
|
|
586
|
+
return index.addresses.filter((a) => underModule(a, address));
|
|
561
587
|
}
|
|
562
588
|
|
|
563
589
|
/** One instance's verdict, before a block aggregates its instances. */
|
|
@@ -608,6 +634,52 @@ export function classifyLiveInstance(address: string, index: LivePlanIndex): Ins
|
|
|
608
634
|
return { kind: "absent" };
|
|
609
635
|
}
|
|
610
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Where one instance address of the declared block `address` sits: its own
|
|
639
|
+
* key and the expanded module path (`@intentius/chant/instance-nodes`), or
|
|
640
|
+
* undefined when it is the block's unexpanded address or another block's.
|
|
641
|
+
* An instance under an expanded module call carries `scope` (#4488), so the
|
|
642
|
+
* live graph draws it under the same id the declared graph lists.
|
|
643
|
+
*/
|
|
644
|
+
function instanceKeyAndScope(address: string, instance: string): { key?: string; scope?: string } | undefined {
|
|
645
|
+
if (instance === address || blockAddressOf(instance) !== address) return undefined;
|
|
646
|
+
const path = terraformInstancePath(instance);
|
|
647
|
+
if (!path) return undefined;
|
|
648
|
+
return { ...(path.key !== undefined ? { key: path.key } : {}), ...(path.scope !== undefined ? { scope: path.scope } : {}) };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Each instance's own verdict (#4454), keyed as Terraform prints it, for the
|
|
653
|
+
* block's `instances`. An instance under an expanded module call carries its
|
|
654
|
+
* module path as `scope` (#4488) and its own key when the block is expanded too.
|
|
655
|
+
*/
|
|
656
|
+
function liveInstanceObservations(address: string, instances: readonly string[], verdicts: readonly InstanceVerdict[]): InstanceObservation[] {
|
|
657
|
+
const out: InstanceObservation[] = [];
|
|
658
|
+
instances.forEach((a, i) => {
|
|
659
|
+
const at = instanceKeyAndScope(address, a);
|
|
660
|
+
if (at === undefined) return;
|
|
661
|
+
const v = verdicts[i]!;
|
|
662
|
+
switch (v.kind) {
|
|
663
|
+
case "owned":
|
|
664
|
+
out.push({ ...at, address: a, verdict: "present", ownership: "owned", status: "bound", ...(v.row.identity ? { physicalId: v.row.identity } : {}) });
|
|
665
|
+
break;
|
|
666
|
+
case "foreign":
|
|
667
|
+
out.push({ ...at, address: a, verdict: "present", ownership: "foreign", status: "unowned", ...(v.row.identity ? { physicalId: v.row.identity } : {}) });
|
|
668
|
+
break;
|
|
669
|
+
case "adoptable":
|
|
670
|
+
out.push({ ...at, address: a, verdict: "present", ownership: "unknown", status: "adoptable", ...(v.row.identity ? { physicalId: v.row.identity } : {}) });
|
|
671
|
+
break;
|
|
672
|
+
case "absent":
|
|
673
|
+
out.push({ ...at, address: a, verdict: "absent" });
|
|
674
|
+
break;
|
|
675
|
+
case "unobserved":
|
|
676
|
+
out.push({ ...at, address: a, verdict: "not-observed", reason: v.reason, detail: v.detail });
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
|
|
611
683
|
/**
|
|
612
684
|
* The declared block's verdict, aggregated over its instances in the
|
|
613
685
|
* precedence the module doc states: not-observed, then foreign, then
|
|
@@ -627,17 +699,22 @@ function readLiveResource(
|
|
|
627
699
|
|
|
628
700
|
const verdicts = instances.map((a) => classifyLiveInstance(a, index));
|
|
629
701
|
const attributes: Record<string, unknown> = { address, root, estate: index.estate };
|
|
630
|
-
|
|
702
|
+
const expanded = instances.length > 1 || instances[0] !== address;
|
|
703
|
+
if (expanded) attributes.instances = instances;
|
|
704
|
+
const perInstance = expanded ? liveInstanceObservations(address, instances, verdicts) : [];
|
|
705
|
+
const withInstances = perInstance.length > 0 ? { instances: perInstance } : {};
|
|
631
706
|
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
707
|
+
const top = rollupInstances(verdicts, TERRAFORM_INSTANCE_ROLLUP);
|
|
708
|
+
|
|
709
|
+
if (top?.kind === "unobserved") {
|
|
710
|
+
return { unobserved: { reason: top.reason, detail: top.detail }, queried };
|
|
635
711
|
}
|
|
636
712
|
|
|
637
|
-
const foreign =
|
|
638
|
-
if (foreign
|
|
713
|
+
const foreign = top;
|
|
714
|
+
if (foreign?.kind === "foreign") {
|
|
639
715
|
return {
|
|
640
716
|
present: {
|
|
717
|
+
...withInstances,
|
|
641
718
|
type: entity.type,
|
|
642
719
|
physicalId: foreign.row.identity ?? address,
|
|
643
720
|
status: "unowned",
|
|
@@ -652,8 +729,8 @@ function readLiveResource(
|
|
|
652
729
|
};
|
|
653
730
|
}
|
|
654
731
|
|
|
655
|
-
const adoptable =
|
|
656
|
-
if (adoptable
|
|
732
|
+
const adoptable = top;
|
|
733
|
+
if (adoptable?.kind === "adoptable") {
|
|
657
734
|
// `unknown`, not `foreign`: the resource matches the declaration and one
|
|
658
735
|
// tag write claims it. The two values that write carries are the whole
|
|
659
736
|
// adoption proposal #2105 builds its Op on, so they ride the metadata
|
|
@@ -661,6 +738,7 @@ function readLiveResource(
|
|
|
661
738
|
// surfaced, because nothing was read off this resource: it carries none.
|
|
662
739
|
return {
|
|
663
740
|
present: {
|
|
741
|
+
...withInstances,
|
|
664
742
|
type: entity.type,
|
|
665
743
|
physicalId: adoptable.row.identity ?? address,
|
|
666
744
|
status: "adoptable",
|
|
@@ -683,13 +761,14 @@ function readLiveResource(
|
|
|
683
761
|
};
|
|
684
762
|
}
|
|
685
763
|
|
|
764
|
+
if (top?.kind !== "owned") return { absent: true, queried };
|
|
686
765
|
const owned = verdicts.filter((v) => v.kind === "owned") as Array<{ kind: "owned"; row: LivePlanBoundRow }>;
|
|
687
|
-
if (owned.length === 0) return { absent: true, queried };
|
|
688
766
|
|
|
689
767
|
const sources = [...new Set(owned.map((v) => v.row.source ?? "unknown"))].sort();
|
|
690
768
|
const byMarker = sources.length === 1 && sources[0] === "marker";
|
|
691
769
|
return {
|
|
692
770
|
present: {
|
|
771
|
+
...withInstances,
|
|
693
772
|
type: entity.type,
|
|
694
773
|
physicalId: owned.length === 1 ? (owned[0]!.row.identity ?? address) : address,
|
|
695
774
|
status: "bound",
|
|
@@ -732,24 +811,46 @@ export function livePlanDocumentFile(root: string): string {
|
|
|
732
811
|
return `${root.replace(/[^A-Za-z0-9._-]/g, "_")}.live-plan.json`;
|
|
733
812
|
}
|
|
734
813
|
|
|
735
|
-
/**
|
|
736
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Write a live root's document where {@link TERRAFORM_LIVE_PLAN_DIR_ENV} asks,
|
|
816
|
+
* if it asks. The path written, or undefined when nothing was.
|
|
817
|
+
*/
|
|
818
|
+
function handLivePlanDocument(root: string, json: unknown): string | undefined {
|
|
737
819
|
const out = process.env[TERRAFORM_LIVE_PLAN_DIR_ENV];
|
|
738
|
-
if (!out || !isAbsolute(out)) return;
|
|
820
|
+
if (!out || !isAbsolute(out)) return undefined;
|
|
739
821
|
try {
|
|
740
822
|
mkdirSync(out, { recursive: true });
|
|
741
|
-
|
|
823
|
+
const file = join(out, livePlanDocumentFile(root));
|
|
824
|
+
writeFileSync(file, JSON.stringify(json));
|
|
825
|
+
return file;
|
|
742
826
|
} catch {
|
|
743
827
|
// Best effort: the read's answer does not depend on the copy.
|
|
828
|
+
return undefined;
|
|
744
829
|
}
|
|
745
830
|
}
|
|
746
831
|
|
|
832
|
+
/**
|
|
833
|
+
* What a live root's bind leaves for the engine graph read (#4528): the
|
|
834
|
+
* `live-plan -json` document it indexed, the file it was handed to under
|
|
835
|
+
* {@link TERRAFORM_LIVE_PLAN_DIR_ENV} if any, and the estate.
|
|
836
|
+
*/
|
|
837
|
+
interface LivePlanHandoff {
|
|
838
|
+
json?: unknown;
|
|
839
|
+
file?: string;
|
|
840
|
+
estate?: string;
|
|
841
|
+
}
|
|
842
|
+
|
|
747
843
|
/**
|
|
748
844
|
* One live root's reader: `live-plan -json`, then address matching against
|
|
749
845
|
* the document. `terraform show` is never run here, and there is no state
|
|
750
846
|
* file to run it over.
|
|
751
847
|
*/
|
|
752
|
-
function liveAdapter(
|
|
848
|
+
function liveAdapter(
|
|
849
|
+
root: string,
|
|
850
|
+
cwd: string | undefined,
|
|
851
|
+
deps: TerraformReadDeps,
|
|
852
|
+
handoff: LivePlanHandoff = {},
|
|
853
|
+
): ObserverAdapter<LivePlanIndex> {
|
|
753
854
|
const where = cwd ? { cwd } : {};
|
|
754
855
|
let dir: string | undefined;
|
|
755
856
|
|
|
@@ -757,8 +858,12 @@ function liveAdapter(root: string, cwd: string | undefined, deps: TerraformReadD
|
|
|
757
858
|
async bind(): Promise<LivePlanIndex> {
|
|
758
859
|
const planned = await deps.livePlan({ root, ...where });
|
|
759
860
|
dir = planned.dir;
|
|
760
|
-
handLivePlanDocument(root, planned.json);
|
|
761
|
-
|
|
861
|
+
const file = handLivePlanDocument(root, planned.json);
|
|
862
|
+
const index = indexLivePlan(planned.json);
|
|
863
|
+
handoff.json = planned.json;
|
|
864
|
+
handoff.estate = planned.estate ?? index.estate;
|
|
865
|
+
if (file) handoff.file = file;
|
|
866
|
+
return index;
|
|
762
867
|
},
|
|
763
868
|
|
|
764
869
|
classifyBindFailure(err) {
|
|
@@ -817,6 +922,204 @@ function liveAdapter(root: string, cwd: string | undefined, deps: TerraformReadD
|
|
|
817
922
|
};
|
|
818
923
|
}
|
|
819
924
|
|
|
925
|
+
/* ─────────────────────── engine facts (#4528) ──────────────────────────── */
|
|
926
|
+
|
|
927
|
+
/** {@link readEngineGraph}'s answer: facts and edges keyed in chant's id space. */
|
|
928
|
+
export interface EngineGraphRead {
|
|
929
|
+
/** Facts of a block node, by entity key. */
|
|
930
|
+
blocks: Map<string, EngineFacts>;
|
|
931
|
+
/** Facts of an instance node, by its block's entity key, then by the instance address. */
|
|
932
|
+
instances: Map<string, Map<string, EngineFacts>>;
|
|
933
|
+
/** Edges with a grain, endpoints as live graph node ids. */
|
|
934
|
+
edges: IREdge[];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const GRAINS: ReadonlySet<string> = new Set<EdgeGrain>(["block", "instance", "property"]);
|
|
938
|
+
const OWNERSHIPS: ReadonlySet<string> = new Set(["bound", "unowned", "pending", "foreign"]);
|
|
939
|
+
const OWNED_BY: ReadonlySet<string> = new Set(["marker", "record", "name"]);
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* One node's `facts`, keeping only the fields of chant's `EngineFacts` and
|
|
943
|
+
* only values of the shape it declares. Undefined when nothing is left.
|
|
944
|
+
*/
|
|
945
|
+
function engineFactsOf(value: unknown): EngineFacts | undefined {
|
|
946
|
+
const f = asRecord(value);
|
|
947
|
+
const out: EngineFacts = {};
|
|
948
|
+
const ownership = asString(f.ownership);
|
|
949
|
+
if (ownership && OWNERSHIPS.has(ownership)) out.ownership = ownership as EngineFacts["ownership"];
|
|
950
|
+
const ownedBy = asString(f.ownedBy);
|
|
951
|
+
if (ownedBy && OWNED_BY.has(ownedBy)) out.ownedBy = ownedBy as EngineFacts["ownedBy"];
|
|
952
|
+
const markers = asRecord(f.markers);
|
|
953
|
+
const encoding = asString(markers.encoding);
|
|
954
|
+
const source = asString(markers.source);
|
|
955
|
+
if ((encoding === "plain" || encoding === "escaped" || encoding === "digest") && (source === "live" || source === "record")) {
|
|
956
|
+
out.markers = {
|
|
957
|
+
...(asString(markers.owner) ? { owner: asString(markers.owner) } : {}),
|
|
958
|
+
...(asString(markers.address) ? { address: asString(markers.address) } : {}),
|
|
959
|
+
encoding,
|
|
960
|
+
source,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const cloudIds = asArray(f.cloudIds)
|
|
964
|
+
.map((c) => ({ kind: asString(asRecord(c).kind), value: asString(asRecord(c).value) }))
|
|
965
|
+
.filter((c): c is NonNullable<EngineFacts["cloudIds"]>[number] =>
|
|
966
|
+
c.value !== undefined && ["import-id", "arn", "provider-id", "self-link", "k8s-uid"].includes(c.kind ?? ""),
|
|
967
|
+
);
|
|
968
|
+
if (cloudIds.length > 0) out.cloudIds = cloudIds;
|
|
969
|
+
const history = asRecord(f.recordVersions);
|
|
970
|
+
if (typeof history.kept === "boolean") {
|
|
971
|
+
out.recordVersions = {
|
|
972
|
+
kept: history.kept,
|
|
973
|
+
versions: asArray(history.versions).flatMap((v) => {
|
|
974
|
+
const r = asRecord(v);
|
|
975
|
+
const id = asString(r.id);
|
|
976
|
+
const writtenAt = asString(r.writtenAt);
|
|
977
|
+
return id && writtenAt ? [{ id, writtenAt, current: r.current === true, deleted: r.deleted === true }] : [];
|
|
978
|
+
}),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const observedAt = asString(f.observedAt);
|
|
982
|
+
if (observedAt) out.observedAt = observedAt;
|
|
983
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Read an engine graph document (`choudoufu live-graph -json`, chant#4527's
|
|
988
|
+
* schema) for one root into chant's id space. Node ids are choudoufu's
|
|
989
|
+
* addresses (the #4520 ruling, Q2): a block node is the entity
|
|
990
|
+
* {@link entityKeyFor} names; an instance node (`instanceOf`) belongs to its
|
|
991
|
+
* block's entity, and as an edge endpoint it is the live instance node id
|
|
992
|
+
* core's `observedInstanceNodeId` gives. A node with no address (an estate
|
|
993
|
+
* the configuration reads, `estate:<name>`) has no chant entity, so its
|
|
994
|
+
* edges are left out. Edges keep their grain; one without a grain is left out.
|
|
995
|
+
*
|
|
996
|
+
* Throws when the document is not an engine graph document.
|
|
997
|
+
*/
|
|
998
|
+
export function readEngineGraph(root: string, document: unknown): EngineGraphRead {
|
|
999
|
+
const doc = asRecord(document);
|
|
1000
|
+
const engineGraph = asRecord(asRecord(doc.meta).engineGraph);
|
|
1001
|
+
if (doc.version !== 1 || engineGraph.format !== 1 || !Array.isArray(doc.nodes)) {
|
|
1002
|
+
throw new Error("not an engine graph document (want GraphIR version 1 with meta.engineGraph format 1)");
|
|
1003
|
+
}
|
|
1004
|
+
const blocks = new Map<string, EngineFacts>();
|
|
1005
|
+
const instances = new Map<string, Map<string, EngineFacts>>();
|
|
1006
|
+
const nodeIds = new Map<string, string>();
|
|
1007
|
+
|
|
1008
|
+
for (const entry of doc.nodes) {
|
|
1009
|
+
const node = asRecord(entry);
|
|
1010
|
+
const id = asString(node.id);
|
|
1011
|
+
const address = asString(asRecord(node.attrs).address);
|
|
1012
|
+
if (!id || !address) continue;
|
|
1013
|
+
const facts = engineFactsOf(node.facts);
|
|
1014
|
+
const instanceOf = asString(node.instanceOf);
|
|
1015
|
+
if (instanceOf) {
|
|
1016
|
+
const block = entityKeyFor(root, instanceOf);
|
|
1017
|
+
const key = asString(node.instanceKey);
|
|
1018
|
+
const scope = asString(node.instanceScope);
|
|
1019
|
+
const liveId = observedInstanceNodeId(block, {
|
|
1020
|
+
address: id,
|
|
1021
|
+
...(key !== undefined ? { key } : {}),
|
|
1022
|
+
...(scope !== undefined ? { scope } : {}),
|
|
1023
|
+
});
|
|
1024
|
+
if (liveId) nodeIds.set(id, liveId);
|
|
1025
|
+
if (facts) {
|
|
1026
|
+
let byAddress = instances.get(block);
|
|
1027
|
+
if (!byAddress) instances.set(block, (byAddress = new Map()));
|
|
1028
|
+
byAddress.set(id, facts);
|
|
1029
|
+
}
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const key = entityKeyFor(root, id);
|
|
1033
|
+
nodeIds.set(id, key);
|
|
1034
|
+
if (facts) blocks.set(key, facts);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const edges: IREdge[] = [];
|
|
1038
|
+
const seen = new Set<string>();
|
|
1039
|
+
for (const entry of asArray(doc.edges)) {
|
|
1040
|
+
const edge = asRecord(entry);
|
|
1041
|
+
const grain = asString(edge.grain);
|
|
1042
|
+
if (!grain || !GRAINS.has(grain)) continue;
|
|
1043
|
+
const from = nodeIds.get(asString(edge.from) ?? "");
|
|
1044
|
+
const to = nodeIds.get(asString(edge.to) ?? "");
|
|
1045
|
+
if (!from || !to || from === to) continue;
|
|
1046
|
+
const viaAttr = asString(edge.viaAttr);
|
|
1047
|
+
const key = `${from}\0${to}\0${viaAttr ?? ""}\0${grain}`;
|
|
1048
|
+
if (seen.has(key)) continue;
|
|
1049
|
+
seen.add(key);
|
|
1050
|
+
edges.push({ from, to, kind: "ref", ...(viaAttr ? { viaAttr } : {}), grain: grain as EdgeGrain });
|
|
1051
|
+
}
|
|
1052
|
+
return { blocks, instances, edges };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* `resources` with {@link readEngineGraph}'s facts on them: a block's on
|
|
1057
|
+
* `ResourceMetadata.facts`, an instance's on its `instances` entry of the
|
|
1058
|
+
* same address. Nothing else changes, and nothing is added for an entity the
|
|
1059
|
+
* read did not return.
|
|
1060
|
+
*/
|
|
1061
|
+
export function withEngineFacts(
|
|
1062
|
+
resources: Record<string, ResourceMetadata>,
|
|
1063
|
+
read: EngineGraphRead,
|
|
1064
|
+
): Record<string, ResourceMetadata> {
|
|
1065
|
+
const out: Record<string, ResourceMetadata> = {};
|
|
1066
|
+
for (const [name, meta] of Object.entries(resources)) {
|
|
1067
|
+
const facts = read.blocks.get(name);
|
|
1068
|
+
const byAddress = read.instances.get(name);
|
|
1069
|
+
const instances = byAddress && meta.instances
|
|
1070
|
+
? meta.instances.map((inst) => {
|
|
1071
|
+
const f = byAddress.get(inst.address);
|
|
1072
|
+
return f ? { ...inst, facts: f } : inst;
|
|
1073
|
+
})
|
|
1074
|
+
: meta.instances;
|
|
1075
|
+
out[name] = {
|
|
1076
|
+
...meta,
|
|
1077
|
+
...(facts ? { facts } : {}),
|
|
1078
|
+
...(instances ? { instances } : {}),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return out;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Run `choudoufu live-graph -json -from-plan=<file>` over the document a
|
|
1086
|
+
* live root's bind handed off, and read it (#4528). Never throws: facts are
|
|
1087
|
+
* extra, so a failure is a note and no facts.
|
|
1088
|
+
*/
|
|
1089
|
+
async function engineGraphFor(
|
|
1090
|
+
root: string,
|
|
1091
|
+
cwd: string | undefined,
|
|
1092
|
+
deps: TerraformReadDeps,
|
|
1093
|
+
handoff: LivePlanHandoff,
|
|
1094
|
+
): Promise<{ read?: EngineGraphRead; note?: string }> {
|
|
1095
|
+
if (!deps.liveGraph || handoff.json === undefined) return {};
|
|
1096
|
+
let scratch: string | undefined;
|
|
1097
|
+
try {
|
|
1098
|
+
let file = handoff.file;
|
|
1099
|
+
if (!file) {
|
|
1100
|
+
scratch = mkdtempSync(join(tmpdir(), "chant-live-graph-"));
|
|
1101
|
+
file = join(scratch, livePlanDocumentFile(root));
|
|
1102
|
+
writeFileSync(file, JSON.stringify(handoff.json));
|
|
1103
|
+
}
|
|
1104
|
+
const graph = await deps.liveGraph({ root, ...(cwd ? { cwd } : {}), fromPlan: file, ...(handoff.estate ? { estate: handoff.estate } : {}) });
|
|
1105
|
+
if (!graph.supported) {
|
|
1106
|
+
return {
|
|
1107
|
+
note:
|
|
1108
|
+
`terraform.roots.${root}: engine facts are absent: this choudoufu has no \`live-graph -from-plan\` ` +
|
|
1109
|
+
`(${graph.detail}), which needs v${MIN_CHOUDOUFU_GRAPH_VERSION} or later; the rest of the read is unchanged`,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return { read: readEngineGraph(root, graph.json) };
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
const message = (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
1115
|
+
return {
|
|
1116
|
+
note: `terraform.roots.${root}: engine facts are absent: \`choudoufu live-graph -json -from-plan\` failed (${message}); the rest of the read is unchanged`,
|
|
1117
|
+
};
|
|
1118
|
+
} finally {
|
|
1119
|
+
if (scratch) rmSync(scratch, { recursive: true, force: true });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
820
1123
|
/* ──────────────────── the estate beyond the declaration ─────────────────── */
|
|
821
1124
|
|
|
822
1125
|
/** One `items[]` entry of a `live-ls -json` listing. */
|
|
@@ -944,7 +1247,8 @@ export interface LiveEstateOptions {
|
|
|
944
1247
|
}
|
|
945
1248
|
|
|
946
1249
|
/**
|
|
947
|
-
* Every live root the project declares, in config order
|
|
1250
|
+
* Every live root the project declares, in config order, leaving out a
|
|
1251
|
+
* `signalOnly` one (#4484). Reads the project
|
|
948
1252
|
* config the same way the activities do, so `terraform.roots` is read once
|
|
949
1253
|
* and the same answer reaches both.
|
|
950
1254
|
*/
|
|
@@ -956,12 +1260,14 @@ export async function liveRootNames(cwd?: string): Promise<Array<{ root: string;
|
|
|
956
1260
|
const start = resolve(cwd ?? process.cwd());
|
|
957
1261
|
const { config, configPath } = await loadChantConfigUpward(start);
|
|
958
1262
|
const projectRoot = configPath ? dirname(configPath) : start;
|
|
959
|
-
const namespace = (config as { terraform?: { binary?: string; roots?: Record<string, { dir: string }> } })
|
|
1263
|
+
const namespace = (config as { terraform?: { binary?: string; roots?: Record<string, { dir: string; signalOnly?: boolean }> } })
|
|
960
1264
|
.terraform;
|
|
961
1265
|
if (namespace?.binary !== "choudoufu") return [];
|
|
962
1266
|
|
|
963
1267
|
const live: Array<{ root: string; estate: string }> = [];
|
|
964
1268
|
for (const [name, root] of Object.entries(namespace.roots ?? {})) {
|
|
1269
|
+
// A `signalOnly` root (#4484) is another project's: it only brings this one into a change.
|
|
1270
|
+
if (root.signalOnly === true) continue;
|
|
965
1271
|
const estate = detectLiveEstate(resolve(projectRoot, root.dir));
|
|
966
1272
|
if (estate !== undefined) live.push({ root: name, estate });
|
|
967
1273
|
}
|
|
@@ -1105,6 +1411,12 @@ export interface TerraformReadDeps {
|
|
|
1105
1411
|
/** The document alone (chant #2498): this reader indexes it and never reads the human render. */
|
|
1106
1412
|
livePlan: typeof choudoufuLivePlanDocument;
|
|
1107
1413
|
liveLs: typeof choudoufuLiveLs;
|
|
1414
|
+
/**
|
|
1415
|
+
* The engine graph read on a live root (#4528). Optional so a test that
|
|
1416
|
+
* never asks for facts need not stub it; omitted, no facts are read and no
|
|
1417
|
+
* note is added.
|
|
1418
|
+
*/
|
|
1419
|
+
liveGraph?: typeof choudoufuLiveGraph;
|
|
1108
1420
|
/**
|
|
1109
1421
|
* The plan depth's read (#4358). Optional so a test that never asks for the
|
|
1110
1422
|
* plan depth need not stub it; omitted, the real activity runs.
|
|
@@ -1117,6 +1429,7 @@ const REAL_DEPS: TerraformReadDeps = {
|
|
|
1117
1429
|
show: terraformShow,
|
|
1118
1430
|
livePlan: choudoufuLivePlanDocument,
|
|
1119
1431
|
liveLs: choudoufuLiveLs,
|
|
1432
|
+
liveGraph: choudoufuLiveGraph,
|
|
1120
1433
|
planRead: terraformPlanRead,
|
|
1121
1434
|
};
|
|
1122
1435
|
|
|
@@ -1222,6 +1535,22 @@ function blockRows(address: string, index: StateIndex): StateResourceRow[] {
|
|
|
1222
1535
|
return [...index.rows.values()].filter((row) => blockAddressOf(row.address) === address);
|
|
1223
1536
|
}
|
|
1224
1537
|
|
|
1538
|
+
/**
|
|
1539
|
+
* Each row of an expanded block as an instance (#4454), keyed as Terraform
|
|
1540
|
+
* prints it. A state row is a recorded instance, owned by membership. A row
|
|
1541
|
+
* under an expanded module call carries its module path as `scope`, as on
|
|
1542
|
+
* the live read (#4488).
|
|
1543
|
+
*/
|
|
1544
|
+
function stateInstanceObservations(address: string, rows: readonly StateResourceRow[]): InstanceObservation[] {
|
|
1545
|
+
const out: InstanceObservation[] = [];
|
|
1546
|
+
for (const r of rows) {
|
|
1547
|
+
const at = instanceKeyAndScope(address, r.address);
|
|
1548
|
+
if (at === undefined) continue;
|
|
1549
|
+
out.push({ ...at, address: r.address, verdict: "present", ownership: "owned", status: r.mode ?? "managed", ...(r.id ? { physicalId: r.id } : {}) });
|
|
1550
|
+
}
|
|
1551
|
+
return out.sort((a, b) => a.address.localeCompare(b.address));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1225
1554
|
/**
|
|
1226
1555
|
* One declared entity against a state index: the state read's whole verdict,
|
|
1227
1556
|
* and the plan depth's too, since a plan's `prior_state` is the same shape.
|
|
@@ -1237,13 +1566,17 @@ function readStateEntity(
|
|
|
1237
1566
|
const { address } = entity as TerraformDeclared;
|
|
1238
1567
|
|
|
1239
1568
|
if (entity.type === RESOURCE_TYPE || entity.type === DATA_TYPE) {
|
|
1240
|
-
// The exact row first, which is the state read's rule since #2087
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1569
|
+
// The exact row first, which is the state read's rule since #2087, then
|
|
1570
|
+
// a `count` or `for_each` block by its instances: the plan depth did
|
|
1571
|
+
// that from the start, and the state read does since #4454, which is
|
|
1572
|
+
// what gives a drift row for `aws_eip.pool[0]` a block to roll up to.
|
|
1573
|
+
const rows = blockRows(address, index);
|
|
1243
1574
|
if (rows.length === 0) return { absent: true, queried };
|
|
1244
1575
|
const row = rows[0]!;
|
|
1576
|
+
const instances = stateInstanceObservations(address, rows);
|
|
1245
1577
|
return {
|
|
1246
1578
|
present: {
|
|
1579
|
+
...(instances.length > 0 ? { instances } : {}),
|
|
1247
1580
|
type: entity.type,
|
|
1248
1581
|
physicalId: rows.length === 1 ? (row.id ?? address) : address,
|
|
1249
1582
|
status: row.mode ?? "managed",
|
|
@@ -1423,9 +1756,10 @@ function livePlanDepthAdapter(
|
|
|
1423
1756
|
cwd: string | undefined,
|
|
1424
1757
|
deps: TerraformReadDeps,
|
|
1425
1758
|
outcome: { answered: LivePlanDepthAnswer; note: string; read?: RootRead },
|
|
1759
|
+
handoff: LivePlanHandoff = {},
|
|
1426
1760
|
): ObserverAdapter<LivePlanDepthIndex> {
|
|
1427
1761
|
const where = cwd ? { cwd } : {};
|
|
1428
|
-
const live = liveAdapter(root, cwd, deps);
|
|
1762
|
+
const live = liveAdapter(root, cwd, deps, handoff);
|
|
1429
1763
|
const planRead = deps.planRead ?? terraformPlanRead;
|
|
1430
1764
|
const firstLine = (err: unknown): string => (err instanceof Error ? err.message : String(err)).split("\n")[0]!;
|
|
1431
1765
|
let command = "";
|
|
@@ -1571,6 +1905,15 @@ export async function describeResources(
|
|
|
1571
1905
|
// an overlay exists to prevent.
|
|
1572
1906
|
const sources: Record<string, string> = {};
|
|
1573
1907
|
const rootReads: Record<string, RootRead> = {};
|
|
1908
|
+
const edges: IREdge[] = [];
|
|
1909
|
+
// A live root's observation with the engine graph's facts and edges on it (#4528).
|
|
1910
|
+
const withEngineGraph = async (root: string, handoff: LivePlanHandoff, observed: ReturnType<typeof normalizeObservation>) => {
|
|
1911
|
+
const graph = await engineGraphFor(root, options.cwd, deps, handoff);
|
|
1912
|
+
if (graph.note) notes.push(graph.note);
|
|
1913
|
+
if (!graph.read) return observed;
|
|
1914
|
+
edges.push(...graph.read.edges);
|
|
1915
|
+
return { ...observed, resources: withEngineFacts(observed.resources, graph.read) };
|
|
1916
|
+
};
|
|
1574
1917
|
const depthOf = byRoot.size > 0 ? await observeDepths(options, notes) : () => "state" as const;
|
|
1575
1918
|
for (const [root, declared] of byRoot) {
|
|
1576
1919
|
// One fact decides the whole read, and the parse already recorded it on
|
|
@@ -1587,15 +1930,18 @@ export async function describeResources(
|
|
|
1587
1930
|
// Both reads (#4371): `live-plan` for ownership, the plan for the
|
|
1588
1931
|
// attributes on what it bound. `sources` is known only after the read.
|
|
1589
1932
|
const outcome: { answered: LivePlanDepthAnswer; note: string; read?: RootRead } = { answered: "live", note: "" };
|
|
1590
|
-
const
|
|
1933
|
+
const handoff: LivePlanHandoff = {};
|
|
1934
|
+
const read = await observeEntities(declared, livePlanDepthAdapter(root, options.cwd, deps, outcome, handoff));
|
|
1591
1935
|
for (const entity of declared) sources[entity.name] = outcome.answered;
|
|
1592
1936
|
if (outcome.read) rootReads[`terraform.roots.${root}`] = outcome.read;
|
|
1593
1937
|
if (outcome.note) notes.push(outcome.note);
|
|
1594
|
-
parts.push(normalizeObservation(read));
|
|
1938
|
+
parts.push(await withEngineGraph(root, handoff, normalizeObservation(read)));
|
|
1595
1939
|
continue;
|
|
1596
1940
|
}
|
|
1597
1941
|
for (const entity of declared) sources[entity.name] = "live";
|
|
1598
|
-
|
|
1942
|
+
const handoff: LivePlanHandoff = {};
|
|
1943
|
+
const read = await observeEntities(declared, liveAdapter(root, options.cwd, deps, handoff));
|
|
1944
|
+
parts.push(await withEngineGraph(root, handoff, normalizeObservation(read)));
|
|
1599
1945
|
continue;
|
|
1600
1946
|
}
|
|
1601
1947
|
if (depth === "plan") {
|
|
@@ -1646,5 +1992,5 @@ export async function describeResources(
|
|
|
1646
1992
|
|
|
1647
1993
|
// A planned create's plan attributes (#4372) ride beside the verdict, never
|
|
1648
1994
|
// in `resources`: the block stays absent.
|
|
1649
|
-
return observation(resources, unobserved, merged.queried, merged.notes, undefined, sources, merged.annotations, rootReads);
|
|
1995
|
+
return observation(resources, unobserved, merged.queried, merged.notes, undefined, sources, merged.annotations, rootReads, edges);
|
|
1650
1996
|
}
|