@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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terraform lexicon's drift read (behold#995): `LexiconPlugin.readDrift`,
|
|
3
|
+
* which `chant lifecycle drift` calls, and which `chant workspace drift` runs
|
|
4
|
+
* in a `terraform` or `choudoufu` member's reader project. It runs the
|
|
5
|
+
* engine's own reads, one root at a time, and hands each document to the
|
|
6
|
+
* drift-record adapters in `./drift-record.ts` (#4451), so the records say
|
|
7
|
+
* exactly what those adapters say.
|
|
8
|
+
*
|
|
9
|
+
* | root | `--read` | reads | adapter |
|
|
10
|
+
* |---|---|---|---|
|
|
11
|
+
* | stock | `state` (default) | `show -json` over state | `terraformStateDriftRecords` |
|
|
12
|
+
* | stock | `plan` | `plan -input=false -lock=false -out=<tmp>`, `show -json` | `terraformPlanDriftRecords` |
|
|
13
|
+
* | live | `ownership` (default) | `choudoufu live-plan -json`, `live-ls -json` | `choudoufuLiveDriftRecords` |
|
|
14
|
+
* | live | `plan` | live-plan, then the same plan read | `choudoufuLivePlusPlanDriftRecords` |
|
|
15
|
+
* | live | `--resource <addr>` | `choudoufu live-plan -resource=<addr>` | its own stream, decoded by `decodeDriftLines` |
|
|
16
|
+
*
|
|
17
|
+
* Nothing is written in the root. `init` is never run, as on the plan read
|
|
18
|
+
* (`terraformPlanRead`): a root that was never initialised reads
|
|
19
|
+
* not-observed with the init line in the detail. A plan read that fails
|
|
20
|
+
* falls back one depth (the plan to state, live+plan to ownership) and the
|
|
21
|
+
* records carry `fellBackFrom`. A live-ls that fails is left out: it only
|
|
22
|
+
* adds what the estate holds and nothing declares.
|
|
23
|
+
*
|
|
24
|
+
* Each root's records go out as one stream: a `drift-start` whose `expected`
|
|
25
|
+
* is every address the read answered, the records, and a `drift-settle`.
|
|
26
|
+
* `node` stays null; `chant workspace drift` places each record on the
|
|
27
|
+
* member's graph. A pulumi member (`pulumiMember`) is refused: its drift is
|
|
28
|
+
* the pulumi lexicon's (chant#4453).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawn } from "node:child_process";
|
|
32
|
+
import { resolve } from "node:path";
|
|
33
|
+
import {
|
|
34
|
+
decodeDriftLines,
|
|
35
|
+
driftRecord,
|
|
36
|
+
driftRecordLine,
|
|
37
|
+
driftSettle,
|
|
38
|
+
driftStart,
|
|
39
|
+
type DriftEngine,
|
|
40
|
+
type DriftLine,
|
|
41
|
+
type DriftRead,
|
|
42
|
+
type DriftRecord,
|
|
43
|
+
type ReadDriftOptions,
|
|
44
|
+
type ReadDriftResult,
|
|
45
|
+
} from "@intentius/chant/drift-record";
|
|
46
|
+
import { ownRoots, type TerraformConfig, type TerraformRootConfig } from "./config";
|
|
47
|
+
import { declaredOf } from "./describe-resources";
|
|
48
|
+
import {
|
|
49
|
+
choudoufuLivePlusPlanDriftRecords,
|
|
50
|
+
choudoufuLiveDriftRecords,
|
|
51
|
+
terraformPlanDriftRecords,
|
|
52
|
+
terraformStateDriftRecords,
|
|
53
|
+
} from "./drift-record";
|
|
54
|
+
import { RESOURCE_TYPE } from "./hcl/parse";
|
|
55
|
+
import { renderTerraformRoots } from "./hcl/roots";
|
|
56
|
+
import { readsAsCredentials } from "./live-plan-reasons";
|
|
57
|
+
import { detectLiveEstate } from "./op/activities/live-detect";
|
|
58
|
+
import {
|
|
59
|
+
choudoufuLiveLs,
|
|
60
|
+
choudoufuLivePlanDocument,
|
|
61
|
+
rootIsInitialised,
|
|
62
|
+
terraformBinary,
|
|
63
|
+
terraformEnvironment,
|
|
64
|
+
terraformInitLine,
|
|
65
|
+
terraformPlanRead,
|
|
66
|
+
terraformShow,
|
|
67
|
+
} from "./op/activities/terraform";
|
|
68
|
+
|
|
69
|
+
const firstLine = (err: unknown): string => (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
70
|
+
|
|
71
|
+
/** The reads a root answers, the first its default. */
|
|
72
|
+
const ROOT_READS: Record<"stock" | "live", readonly DriftRead[]> = { stock: ["state", "plan"], live: ["ownership", "plan"] };
|
|
73
|
+
|
|
74
|
+
interface RootContext {
|
|
75
|
+
options: ReadDriftOptions;
|
|
76
|
+
name: string;
|
|
77
|
+
root: TerraformRootConfig;
|
|
78
|
+
binary: string;
|
|
79
|
+
dir: string;
|
|
80
|
+
engine: DriftEngine;
|
|
81
|
+
/** Every declared managed resource's address. */
|
|
82
|
+
declared: string[];
|
|
83
|
+
where: { root: string; cwd: string };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One root's records as a stream: start, records, settle. */
|
|
87
|
+
function asStream(ctx: RootContext, read: DriftRead, records: DriftRecord[], extra: { fellBackFrom?: string; resource?: string } = {}): DriftLine[] {
|
|
88
|
+
const start = driftStart({
|
|
89
|
+
member: ctx.options.member,
|
|
90
|
+
engine: ctx.engine,
|
|
91
|
+
read,
|
|
92
|
+
...extra,
|
|
93
|
+
at: ctx.options.observedAt,
|
|
94
|
+
expected: records.map((r) => r.address),
|
|
95
|
+
evidence: { tool: ctx.engine },
|
|
96
|
+
});
|
|
97
|
+
const settled = driftSettle(start, records, ctx.options.observedAt);
|
|
98
|
+
return [start, ...settled.records.map(driftRecordLine), settled.line];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Every declared address not-observed, for a read that could not run at all. */
|
|
102
|
+
function unobservedAll(ctx: RootContext, read: DriftRead, err: unknown, command: string): DriftRecord[] {
|
|
103
|
+
const detail = firstLine(err);
|
|
104
|
+
return ctx.declared.map((address) =>
|
|
105
|
+
driftRecord({
|
|
106
|
+
member: ctx.options.member,
|
|
107
|
+
engine: ctx.engine,
|
|
108
|
+
address,
|
|
109
|
+
node: null,
|
|
110
|
+
observedAt: ctx.options.observedAt,
|
|
111
|
+
read,
|
|
112
|
+
status: "not-observed",
|
|
113
|
+
unobserved: { reason: readsAsCredentials(detail) ? "no-credentials" : "read-failed", detail },
|
|
114
|
+
evidence: { tool: ctx.engine, command },
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A stock root's state read. Never runs init. */
|
|
120
|
+
async function stateRecords(ctx: RootContext, fellBackFrom?: string): Promise<DriftRecord[]> {
|
|
121
|
+
const base = { member: ctx.options.member, observedAt: ctx.options.observedAt, engine: ctx.engine as "terraform" | "tofu", declared: ctx.declared, ...(fellBackFrom ? { fellBackFrom } : {}) };
|
|
122
|
+
const env = { ...process.env, ...terraformEnvironment(ctx.root) };
|
|
123
|
+
if (!rootIsInitialised(ctx.dir, env)) {
|
|
124
|
+
const line = terraformInitLine({
|
|
125
|
+
binary: ctx.binary,
|
|
126
|
+
dir: ctx.dir,
|
|
127
|
+
...(ctx.root.backendConfig ? { backendConfig: ctx.root.backendConfig } : {}),
|
|
128
|
+
...(ctx.root.backendConfigFiles ? { backendConfigFiles: ctx.root.backendConfigFiles } : {}),
|
|
129
|
+
});
|
|
130
|
+
return terraformStateDriftRecords({ ...base, failed: { reason: "read-failed", detail: `${ctx.dir} has never been initialised: run \`${line}\` first; chant never runs init on a read` } });
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const shown = await terraformShow(ctx.where);
|
|
134
|
+
return terraformStateDriftRecords({ ...base, state: shown.json });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
const detail = firstLine(err);
|
|
137
|
+
return terraformStateDriftRecords({ ...base, failed: { reason: readsAsCredentials(detail) ? "no-credentials" : "read-failed", detail } });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function stockRoot(ctx: RootContext, read: DriftRead): Promise<DriftLine[]> {
|
|
142
|
+
if (read === "state") return asStream(ctx, "state", await stateRecords(ctx));
|
|
143
|
+
try {
|
|
144
|
+
const plan = await terraformPlanRead(ctx.where);
|
|
145
|
+
return asStream(ctx, "plan", terraformPlanDriftRecords({ member: ctx.options.member, observedAt: ctx.options.observedAt, engine: ctx.engine as "terraform" | "tofu", plan: plan.json }));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// One depth down: what the last apply recorded, said as such.
|
|
148
|
+
process.stderr.write(`terraform.roots.${ctx.name}: the plan read failed (${firstLine(err)}), so the state read answered\n`);
|
|
149
|
+
return asStream(ctx, "state", await stateRecords(ctx, "plan"), { fellBackFrom: "plan" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function liveOwnership(ctx: RootContext, fellBackFrom?: string): Promise<{ records: DriftRecord[]; livePlan?: unknown; liveLs?: unknown }> {
|
|
154
|
+
let livePlan: unknown;
|
|
155
|
+
try {
|
|
156
|
+
livePlan = (await choudoufuLivePlanDocument(ctx.where)).json;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { records: unobservedAll(ctx, "ownership", err, "choudoufu live-plan -json") };
|
|
159
|
+
}
|
|
160
|
+
let liveLs: unknown;
|
|
161
|
+
try {
|
|
162
|
+
liveLs = (await choudoufuLiveLs({ ...ctx.where, consistent: true })).json;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
process.stderr.write(`terraform.roots.${ctx.name}: live-ls failed (${firstLine(err)}); what the estate holds and nothing declares is left out\n`);
|
|
165
|
+
}
|
|
166
|
+
const base = { member: ctx.options.member, observedAt: ctx.options.observedAt, livePlan, ...(liveLs !== undefined ? { liveLs } : {}), ...(fellBackFrom ? { fellBackFrom } : {}) };
|
|
167
|
+
return { records: choudoufuLiveDriftRecords(base), livePlan, liveLs };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function liveRoot(ctx: RootContext, read: DriftRead): Promise<DriftLine[]> {
|
|
171
|
+
if (read === "ownership") return asStream(ctx, "ownership", (await liveOwnership(ctx)).records);
|
|
172
|
+
const owned = await liveOwnership(ctx);
|
|
173
|
+
if (owned.livePlan === undefined) return asStream(ctx, "ownership", owned.records, { fellBackFrom: "live+plan" });
|
|
174
|
+
try {
|
|
175
|
+
const plan = await terraformPlanRead(ctx.where);
|
|
176
|
+
const records = choudoufuLivePlusPlanDriftRecords({
|
|
177
|
+
member: ctx.options.member,
|
|
178
|
+
observedAt: ctx.options.observedAt,
|
|
179
|
+
livePlan: owned.livePlan,
|
|
180
|
+
plan: plan.json,
|
|
181
|
+
...(owned.liveLs !== undefined ? { liveLs: owned.liveLs } : {}),
|
|
182
|
+
});
|
|
183
|
+
return asStream(ctx, "plan", records);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
process.stderr.write(`terraform.roots.${ctx.name}: the plan read failed (${firstLine(err)}), so live-plan alone answered\n`);
|
|
186
|
+
const fell = choudoufuLiveDriftRecords({
|
|
187
|
+
member: ctx.options.member,
|
|
188
|
+
observedAt: ctx.options.observedAt,
|
|
189
|
+
livePlan: owned.livePlan,
|
|
190
|
+
...(owned.liveLs !== undefined ? { liveLs: owned.liveLs } : {}),
|
|
191
|
+
fellBackFrom: "live+plan",
|
|
192
|
+
});
|
|
193
|
+
return asStream(ctx, "ownership", fell, { fellBackFrom: "live+plan" });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `choudoufu live-plan -resource=<addr>`'s own stream (choudoufu#2245), decoded, with the workspace member's name on every line. */
|
|
198
|
+
async function liveResource(ctx: RootContext, address: string): Promise<DriftLine[]> {
|
|
199
|
+
// `-estate` only for a root that names its estate in chant.config and not in its HCL: choudoufu refuses the flag beside a declaration.
|
|
200
|
+
const estateFlag = ctx.root.estate !== undefined && detectLiveEstate(ctx.dir) === undefined ? [`-estate=${ctx.root.estate}`] : [];
|
|
201
|
+
const argv = ["live-plan", ...estateFlag, `-resource=${address}`];
|
|
202
|
+
const command = `${ctx.binary} ${argv.join(" ")}`;
|
|
203
|
+
const ran = await new Promise<{ code: number | null; stdout: string; stderr: string; error?: Error }>((done) => {
|
|
204
|
+
const child = spawn(ctx.binary, argv, { cwd: ctx.dir, env: { ...process.env, ...terraformEnvironment(ctx.root) }, stdio: ["ignore", "pipe", "pipe"] });
|
|
205
|
+
let stdout = "";
|
|
206
|
+
let stderr = "";
|
|
207
|
+
child.stdout.on("data", (d) => (stdout += String(d)));
|
|
208
|
+
child.stderr.on("data", (d) => (stderr += String(d)));
|
|
209
|
+
child.on("error", (error) => done({ code: null, stdout, stderr, error }));
|
|
210
|
+
child.on("close", (code) => done({ code, stdout, stderr }));
|
|
211
|
+
});
|
|
212
|
+
if (ran.stderr.trim()) process.stderr.write(ran.stderr.endsWith("\n") ? ran.stderr : `${ran.stderr}\n`);
|
|
213
|
+
// 2 is live-plan's "changes present" under -detailed-exitcode; the stream is what answers.
|
|
214
|
+
if (ran.error === undefined && (ran.code === 0 || ran.code === 2) && ran.stdout.trim()) {
|
|
215
|
+
try {
|
|
216
|
+
return decodeDriftLines(ran.stdout).map((line) => ({ ...line, member: ctx.options.member }));
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return asStream(ctx, "plan", [unobservedOne(ctx, address, `${command} printed a line that is not drift: ${firstLine(err)}`, command)], { resource: address });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const why = ran.error?.message ?? (ran.stderr.trim().split("\n").pop() || `exit ${ran.code}`);
|
|
222
|
+
return asStream(ctx, "plan", [unobservedOne(ctx, address, `${command} failed: ${why}`, command)], { resource: address });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function unobservedOne(ctx: RootContext, address: string, detail: string, command: string): DriftRecord {
|
|
226
|
+
return driftRecord({
|
|
227
|
+
member: ctx.options.member,
|
|
228
|
+
engine: "choudoufu",
|
|
229
|
+
address,
|
|
230
|
+
node: null,
|
|
231
|
+
observedAt: ctx.options.observedAt,
|
|
232
|
+
read: "plan",
|
|
233
|
+
status: "not-observed",
|
|
234
|
+
unobserved: { reason: readsAsCredentials(detail) ? "no-credentials" : "read-failed", detail },
|
|
235
|
+
evidence: { tool: "choudoufu", command },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** `LexiconPlugin.readDrift` for the terraform lexicon. */
|
|
240
|
+
export async function readTerraformDrift(options: ReadDriftOptions): Promise<ReadDriftResult> {
|
|
241
|
+
const namespace = (options.config as { terraform?: TerraformConfig }).terraform;
|
|
242
|
+
// A `signalOnly` root (#4484) is not this project's to read.
|
|
243
|
+
const roots = Object.entries(ownRoots(namespace?.roots));
|
|
244
|
+
if (roots.length === 0) return { refused: { code: "no-drift-read", message: "terraform.roots declares no root of its own" } };
|
|
245
|
+
const binary = terraformBinary(namespace);
|
|
246
|
+
const live = (root: TerraformRootConfig, dir: string) => binary === "choudoufu" && (detectLiveEstate(dir) !== undefined || root.estate !== undefined);
|
|
247
|
+
|
|
248
|
+
const contexts: Array<RootContext & { live: boolean }> = [];
|
|
249
|
+
for (const [name, root] of roots) {
|
|
250
|
+
const dir = resolve(options.projectRoot, root.dir);
|
|
251
|
+
const rendered = await renderTerraformRoots({
|
|
252
|
+
projectRoot: options.projectRoot,
|
|
253
|
+
roots: { [name]: root },
|
|
254
|
+
binary: namespace?.binary,
|
|
255
|
+
callModuleType: namespace?.callModuleType,
|
|
256
|
+
moduleRoot: namespace?.moduleRoot,
|
|
257
|
+
});
|
|
258
|
+
const declared = [...rendered.entities.entries()]
|
|
259
|
+
.map(([key, entity]) => declaredOf(key, entity as unknown as { entityType: string; props: Record<string, unknown> }))
|
|
260
|
+
.filter((d) => d.type === RESOURCE_TYPE)
|
|
261
|
+
.map((d) => d.address);
|
|
262
|
+
const isLive = live(root, dir);
|
|
263
|
+
contexts.push({ options, name, root, binary, dir, engine: isLive ? "choudoufu" : binary === "tofu" ? "tofu" : "terraform", declared: [...new Set(declared)].sort(), where: { root: name, cwd: options.projectRoot }, live: isLive });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const ctx of contexts) {
|
|
267
|
+
const reads = ROOT_READS[ctx.live ? "live" : "stock"];
|
|
268
|
+
if (options.read !== undefined && !reads.includes(options.read)) {
|
|
269
|
+
return { refused: { code: "read-unsupported", message: `terraform.roots.${ctx.name} is a ${ctx.live ? "live (choudoufu)" : "stock"} root, which answers ${reads.join(" or ")}, not ${options.read}` } };
|
|
270
|
+
}
|
|
271
|
+
if (options.resource !== undefined && !ctx.live) {
|
|
272
|
+
return { refused: { code: "resource-unsupported", message: `terraform.roots.${ctx.name} is a stock root; one resource is read through choudoufu live-plan -resource, on a live root` } };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lines: DriftLine[] = [];
|
|
277
|
+
for (const ctx of contexts) {
|
|
278
|
+
const read = options.read ?? ROOT_READS[ctx.live ? "live" : "stock"][0];
|
|
279
|
+
if (options.resource !== undefined) lines.push(...(await liveResource(ctx, options.resource)));
|
|
280
|
+
else if (ctx.live) lines.push(...(await liveRoot(ctx, read)));
|
|
281
|
+
else lines.push(...(await stockRoot(ctx, read)));
|
|
282
|
+
}
|
|
283
|
+
return { lines };
|
|
284
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terraform-family drift adapters (#4451) over recorded and hand-written
|
|
3
|
+
* fixtures: a stock root's state and plan, and a choudoufu live root's
|
|
4
|
+
* live-plan, live-ls and plan. Every adapter's records round-trip through
|
|
5
|
+
* the document reader, and no state read yields in-sync or drifted.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { driftDocument, parseDriftDocument, STATE_READ_STATUSES, type DriftRecord } from "@intentius/chant/drift-record";
|
|
12
|
+
import {
|
|
13
|
+
choudoufuLiveDriftRecords,
|
|
14
|
+
choudoufuLiveLsDriftRecords,
|
|
15
|
+
choudoufuLivePlanDriftRecords,
|
|
16
|
+
choudoufuLivePlusPlanDriftRecords,
|
|
17
|
+
terraformPlanDriftRecords,
|
|
18
|
+
terraformStateDriftRecords,
|
|
19
|
+
} from "./drift-record";
|
|
20
|
+
|
|
21
|
+
const FIX = join(import.meta.dirname, "__fixtures__");
|
|
22
|
+
const read = (...path: string[]): unknown => JSON.parse(readFileSync(join(FIX, ...path), "utf-8"));
|
|
23
|
+
const AT = "2026-10-25T12:00:00Z";
|
|
24
|
+
const statusOf = (records: DriftRecord[]) => Object.fromEntries(records.map((r) => [r.address, r.status]));
|
|
25
|
+
const at = (records: DriftRecord[], address: string) => records.find((r) => r.address === address)!;
|
|
26
|
+
|
|
27
|
+
function roundTrip(records: DriftRecord[]): void {
|
|
28
|
+
const doc = driftDocument(records);
|
|
29
|
+
expect(parseDriftDocument(JSON.parse(JSON.stringify(doc)))).toEqual(doc);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("a stock root's state", () => {
|
|
33
|
+
const records = terraformStateDriftRecords({
|
|
34
|
+
member: "app",
|
|
35
|
+
observedAt: AT,
|
|
36
|
+
state: read("show-state.json"),
|
|
37
|
+
declared: ["null_resource.first", "null_resource.second", "module.cdn.null_resource.edge", "null_resource.never_applied"],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("is recorded where state has it and pending where it does not", () => {
|
|
41
|
+
expect(statusOf(records)).toEqual({
|
|
42
|
+
"module.cdn.null_resource.edge": "recorded",
|
|
43
|
+
"null_resource.first": "recorded",
|
|
44
|
+
"null_resource.never_applied": "pending",
|
|
45
|
+
"null_resource.second": "recorded",
|
|
46
|
+
});
|
|
47
|
+
expect(at(records, "null_resource.first").ownership).toEqual({ verdict: "owned", by: "state" });
|
|
48
|
+
roundTrip(records);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("never yields in-sync or drifted, a failed read included", () => {
|
|
52
|
+
const failed = terraformStateDriftRecords({ member: "app", observedAt: AT, declared: ["null_resource.first"], failed: { reason: "read-failed", detail: "init failed" } });
|
|
53
|
+
expect(statusOf(failed)).toEqual({ "null_resource.first": "not-observed" });
|
|
54
|
+
for (const r of [...records, ...failed]) {
|
|
55
|
+
expect(r.read).toBe("state");
|
|
56
|
+
expect(STATE_READ_STATUSES).toContain(r.status);
|
|
57
|
+
expect(r.status).not.toBe("in-sync");
|
|
58
|
+
expect(r.status).not.toBe("drifted");
|
|
59
|
+
}
|
|
60
|
+
roundTrip(failed);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("a stock root's plan", () => {
|
|
65
|
+
const records = terraformPlanDriftRecords({ member: "app", observedAt: AT, plan: read("show-plan.json") });
|
|
66
|
+
|
|
67
|
+
test("drift rows decide, and a configuration change is not drift", () => {
|
|
68
|
+
expect(statusOf(records)).toEqual({
|
|
69
|
+
"shell_script.conf": "drifted",
|
|
70
|
+
"terraform_data.downstream": "in-sync",
|
|
71
|
+
"terraform_data.reader": "in-sync",
|
|
72
|
+
"terraform_data.replaced": "in-sync",
|
|
73
|
+
"terraform_data.updated": "in-sync",
|
|
74
|
+
});
|
|
75
|
+
const conf = at(records, "shell_script.conf");
|
|
76
|
+
expect(conf.read).toBe("plan");
|
|
77
|
+
expect(conf.evidence).toMatchObject({ tool: "terraform", version: "1.15.8" });
|
|
78
|
+
expect(conf.attributes).toContainEqual({ path: "dirty", before: false, after: true });
|
|
79
|
+
roundTrip(records);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a drift delete is missing, a create with no prior row pending, a dropped block undeclared", () => {
|
|
83
|
+
const plan = {
|
|
84
|
+
terraform_version: "1.9.0",
|
|
85
|
+
prior_state: { values: { root_module: { resources: [{ address: "aws_s3_bucket.gone", mode: "managed", type: "aws_s3_bucket" }, { address: "aws_s3_bucket.dropped", mode: "managed", type: "aws_s3_bucket" }] } } },
|
|
86
|
+
resource_drift: [{ address: "aws_s3_bucket.gone", type: "aws_s3_bucket", change: { actions: ["delete"], before: { bucket: "gone" }, after: null } }],
|
|
87
|
+
resource_changes: [
|
|
88
|
+
{ address: "aws_s3_bucket.gone", mode: "managed", type: "aws_s3_bucket", change: { actions: ["create"], before: null, after: { bucket: "gone" } } },
|
|
89
|
+
{ address: "aws_s3_bucket.new", mode: "managed", type: "aws_s3_bucket", change: { actions: ["create"], before: null, after: { bucket: "new" } } },
|
|
90
|
+
{ address: "aws_s3_bucket.dropped", mode: "managed", type: "aws_s3_bucket", change: { actions: ["delete"], before: { bucket: "dropped" }, after: null } },
|
|
91
|
+
{ address: "data.aws_caller_identity.me", mode: "data", type: "aws_caller_identity", change: { actions: ["read"] } },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
const out = terraformPlanDriftRecords({ member: "app", engine: "tofu", observedAt: AT, plan });
|
|
95
|
+
expect(statusOf(out)).toEqual({ "aws_s3_bucket.dropped": "undeclared", "aws_s3_bucket.gone": "missing", "aws_s3_bucket.new": "pending" });
|
|
96
|
+
expect(out.every((r) => r.engine === "tofu" && r.resourceType === "aws_s3_bucket")).toBe(true);
|
|
97
|
+
roundTrip(out);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("choudoufu live-plan", () => {
|
|
102
|
+
const records = choudoufuLivePlanDriftRecords({ member: "estate", observedAt: AT, livePlan: read("live-plan.json") });
|
|
103
|
+
|
|
104
|
+
test("maps bound, unowned, adoptable and omissions", () => {
|
|
105
|
+
expect(statusOf(records)).toEqual({
|
|
106
|
+
"aws_cloudwatch_log_group.adoptable": "unclaimed",
|
|
107
|
+
"aws_cloudwatch_log_group.app": "in-sync",
|
|
108
|
+
"aws_cloudwatch_log_group.held_elsewhere": "unclaimed",
|
|
109
|
+
"aws_cloudwatch_log_group.never_applied": "pending",
|
|
110
|
+
"aws_eip.pool[0]": "in-sync",
|
|
111
|
+
"aws_eip.pool[1]": "in-sync",
|
|
112
|
+
"aws_security_group.main": "in-sync",
|
|
113
|
+
"aws_security_group_rule.https": "pending",
|
|
114
|
+
"aws_subnet.app": "in-sync",
|
|
115
|
+
"aws_vpc.adoptable": "unclaimed",
|
|
116
|
+
"aws_vpc.main": "in-sync",
|
|
117
|
+
});
|
|
118
|
+
expect(records.every((r) => r.read === "ownership" && r.engine === "choudoufu")).toBe(true);
|
|
119
|
+
expect(at(records, "aws_eip.pool[0]").ownership).toEqual({ verdict: "owned", by: "marker", markerOnObject: true });
|
|
120
|
+
expect(at(records, "aws_cloudwatch_log_group.app").ownership).toEqual({ verdict: "owned", by: "derived", markerOnObject: false });
|
|
121
|
+
expect(at(records, "aws_cloudwatch_log_group.adoptable").ownership).toMatchObject({ verdict: "adoptable", adopt: "aws_cloudwatch_log_group.adoptable" });
|
|
122
|
+
expect(at(records, "aws_cloudwatch_log_group.held_elsewhere").ownership).toEqual({ verdict: "foreign", by: "marker", markerOnObject: true, ownedBy: "other-estate" });
|
|
123
|
+
expect(at(records, "aws_cloudwatch_log_group.held_elsewhere").evidence.engineCode).toBe("UNOWNED");
|
|
124
|
+
expect(at(records, "aws_vpc.adoptable").evidence.engineCode).toBe("NEEDS_DISCOVERY");
|
|
125
|
+
expect(at(records, "aws_cloudwatch_log_group.never_applied").evidence.engineCode).toBe("ABSENT");
|
|
126
|
+
expect(at(records, "aws_eip.pool[0]").evidence.version).toBe("v0.15.0");
|
|
127
|
+
roundTrip(records);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("foreign, controller_held, the new omission reasons and an UNOWNED with no row", () => {
|
|
131
|
+
const out = choudoufuLivePlanDriftRecords({ member: "estate", observedAt: AT, livePlan: read("drift-record", "live-plan-sections.json") });
|
|
132
|
+
expect(statusOf(out)).toEqual({
|
|
133
|
+
"arn:aws:s3:::ack-made": "runtime",
|
|
134
|
+
"arn:aws:s3:::someone-elses": "undeclared",
|
|
135
|
+
"aws_cloudwatch_log_group.orphan_unowned": "not-observed",
|
|
136
|
+
"aws_iam_role.app": "in-sync",
|
|
137
|
+
"aws_instance.verified": "not-observed",
|
|
138
|
+
"aws_lambda_function.fn": "not-observed",
|
|
139
|
+
"aws_s3_bucket.records": "in-sync",
|
|
140
|
+
"aws_sqs_queue.later": "not-observed",
|
|
141
|
+
"https://sqs.us-east-1.amazonaws.com/000000000000/loose": "undeclared",
|
|
142
|
+
"kubernetes_deployment.legacy": "runtime",
|
|
143
|
+
"kubernetes_manifest.issuer": "not-observed",
|
|
144
|
+
});
|
|
145
|
+
const reason = (a: string) => [at(out, a).unobserved?.reason, at(out, a).evidence.engineCode];
|
|
146
|
+
expect(reason("aws_cloudwatch_log_group.orphan_unowned")).toEqual(["read-failed", "UNOWNED"]);
|
|
147
|
+
expect(reason("aws_instance.verified")).toEqual(["read-failed", "VERIFIED_NOT_IMPORTABLE"]);
|
|
148
|
+
expect(reason("kubernetes_manifest.issuer")).toEqual(["filtered", "OUT_OF_SCOPE"]);
|
|
149
|
+
expect(reason("aws_lambda_function.fn")).toEqual(["no-credentials", "FAILED"]);
|
|
150
|
+
expect(reason("aws_sqs_queue.later")).toEqual(["read-failed", "SOMETHING_NEW"]);
|
|
151
|
+
expect(at(out, "arn:aws:s3:::someone-elses").ownership).toEqual({ verdict: "foreign", by: "marker", markerOnObject: true, ownedBy: "other-estate" });
|
|
152
|
+
expect(at(out, "https://sqs.us-east-1.amazonaws.com/000000000000/loose").ownership).toEqual({ verdict: "foreign", markerOnObject: false });
|
|
153
|
+
expect(at(out, "kubernetes_deployment.legacy").ownership).toEqual({ verdict: "foreign", by: "label", ownedBy: "Helm release web" });
|
|
154
|
+
expect(at(out, "aws_s3_bucket.records").ownership).toEqual({ verdict: "owned", by: "record", markerOnObject: false });
|
|
155
|
+
roundTrip(out);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a graph places instances on their instance nodes", () => {
|
|
159
|
+
const placed = choudoufuLivePlanDriftRecords({
|
|
160
|
+
member: "estate",
|
|
161
|
+
observedAt: AT,
|
|
162
|
+
livePlan: read("live-plan.json"),
|
|
163
|
+
graph: { member: null, at: null, ids: ["estate/aws_eip.pool", "estate/aws_vpc.main"] },
|
|
164
|
+
});
|
|
165
|
+
expect(at(placed, "aws_eip.pool[1]")).toMatchObject({ node: "estate/aws_eip.pool[1]", instanceOf: "estate/aws_eip.pool" });
|
|
166
|
+
expect(at(placed, "aws_vpc.main").node).toBe("estate/aws_vpc.main");
|
|
167
|
+
expect(at(placed, "aws_subnet.app").node).toBeNull();
|
|
168
|
+
roundTrip(placed);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("choudoufu live-ls", () => {
|
|
173
|
+
test("an undeclared item is undeclared, and a gap live-plan answered is left out", () => {
|
|
174
|
+
const alone = choudoufuLiveLsDriftRecords({ member: "estate", observedAt: AT, liveLs: read("live-ls.json") });
|
|
175
|
+
expect(statusOf(alone)).toEqual({ "aws_s3_bucket.data": "undeclared", "aws_security_group_rule.https": "not-observed" });
|
|
176
|
+
expect(at(alone, "aws_s3_bucket.data").ownership).toEqual({ verdict: "owned", by: "marker", markerOnObject: true });
|
|
177
|
+
expect(at(alone, "aws_security_group_rule.https").evidence.engineCode).toBe("declaration-carried");
|
|
178
|
+
roundTrip(alone);
|
|
179
|
+
|
|
180
|
+
const both = choudoufuLiveDriftRecords({ member: "estate", observedAt: AT, livePlan: read("live-plan.json"), liveLs: read("live-ls.json") });
|
|
181
|
+
expect(at(both, "aws_s3_bucket.data").status).toBe("undeclared");
|
|
182
|
+
expect(at(both, "aws_security_group_rule.https").status).toBe("pending");
|
|
183
|
+
roundTrip(both);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("choudoufu live+plan", () => {
|
|
188
|
+
test("a bound instance with a planned update is drifted, declared before and live after", () => {
|
|
189
|
+
const records = choudoufuLivePlusPlanDriftRecords({
|
|
190
|
+
member: "estate",
|
|
191
|
+
observedAt: AT,
|
|
192
|
+
livePlan: read("live-plan-depth", "live-plan.json"),
|
|
193
|
+
plan: read("live-plan-depth", "show-plan.json"),
|
|
194
|
+
});
|
|
195
|
+
const app = at(records, "aws_cloudwatch_log_group.app");
|
|
196
|
+
expect(app.read).toBe("plan");
|
|
197
|
+
expect(app.status).toBe("drifted");
|
|
198
|
+
expect(app.attributes).toContainEqual({ path: "retention_in_days", before: 1, after: 3 });
|
|
199
|
+
expect(app.ownership).toEqual({ verdict: "owned", by: "derived", markerOnObject: false });
|
|
200
|
+
const vpc = at(records, "aws_vpc.main");
|
|
201
|
+
expect(vpc.status).toBe("drifted");
|
|
202
|
+
expect(vpc.attributes).toContainEqual({ path: "tags.drifted", after: "yes" });
|
|
203
|
+
expect(at(records, "aws_eip.pool[0]")).toMatchObject({ read: "plan", status: "in-sync" });
|
|
204
|
+
// Not bound: the plan does not say who holds it, so live-plan's record stands.
|
|
205
|
+
expect(at(records, "aws_cloudwatch_log_group.never_applied")).toMatchObject({ read: "ownership", status: "pending" });
|
|
206
|
+
roundTrip(records);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a sensitive value never leaves, on either side", () => {
|
|
210
|
+
const records = choudoufuLivePlusPlanDriftRecords({
|
|
211
|
+
member: "estate",
|
|
212
|
+
observedAt: AT,
|
|
213
|
+
livePlan: read("live-plan-depth", "live-plan.json"),
|
|
214
|
+
plan: read("live-plan-depth", "show-plan-sensitive.json"),
|
|
215
|
+
});
|
|
216
|
+
const vpc = at(records, "aws_vpc.main");
|
|
217
|
+
expect(vpc.attributes).toContainEqual({ path: "tags.token", sensitive: true });
|
|
218
|
+
const text = JSON.stringify(driftDocument(records));
|
|
219
|
+
expect(text).not.toContain("hunter2");
|
|
220
|
+
expect(text).not.toContain("hunter3");
|
|
221
|
+
roundTrip(records);
|
|
222
|
+
});
|
|
223
|
+
});
|