@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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraformPlanShape` (#4436), against the recorded `show -json` plans of
|
|
3
|
+
* terraform, tofu and choudoufu in `__fixtures__/change-set/` (see
|
|
4
|
+
* change-set.test.ts for how they were recorded).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { planShapeCovers, planShapeDigest, type PlanShapeEntry } from "@intentius/chant/components/covered-round";
|
|
11
|
+
import { terraformPlanShape } from "./plan-shape";
|
|
12
|
+
|
|
13
|
+
const DIR = join(import.meta.dirname, "__fixtures__", "change-set");
|
|
14
|
+
const read = (name: string): unknown => JSON.parse(readFileSync(join(DIR, name), "utf-8"));
|
|
15
|
+
|
|
16
|
+
const T = "terraform_data";
|
|
17
|
+
|
|
18
|
+
/** The fixture plan: a no-op, an update, an update of a sensitive value, a delete, a replace, two creates under a module. */
|
|
19
|
+
const FIXTURE_ENTRIES: PlanShapeEntry[] = [
|
|
20
|
+
{ logicalId: "module.app.terraform_data.instance[0]", action: "create", type: T, properties: ["id", "input", "output"], unknown: ["id", "output"] },
|
|
21
|
+
{ logicalId: "module.app.terraform_data.instance[1]", action: "create", type: T, properties: ["id", "input", "output"], unknown: ["id", "output"] },
|
|
22
|
+
{ logicalId: "terraform_data.config", action: "update", replacement: "false", type: T, properties: ["input", "output"], unknown: ["output"] },
|
|
23
|
+
{ logicalId: "terraform_data.old", action: "delete", type: T, properties: [], unknown: [] },
|
|
24
|
+
{ logicalId: "terraform_data.secret", action: "update", replacement: "false", type: T, properties: ["input", "output"], unknown: ["output"] },
|
|
25
|
+
{ logicalId: "terraform_data.worker", action: "replace", replacement: "true", type: T, properties: ["id", "output", "triggers_replace"], unknown: ["id", "output"] },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
describe("terraformPlanShape, on real plans", () => {
|
|
29
|
+
test.each(["terraform", "tofu", "choudoufu"] as const)("%s: one entry per change, none for the no-op", (planner) => {
|
|
30
|
+
expect(terraformPlanShape(read(`${planner}.plan.json`), "estate")).toEqual({ member: "estate", entries: FIXTURE_ENTRIES, unknownInputs: [] });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a table of the actions, replacement and attributes the fixture gives", () => {
|
|
34
|
+
const shape = terraformPlanShape(read("terraform.plan.json"), "estate");
|
|
35
|
+
expect(shape.entries.map((e) => [e.logicalId, e.action, e.replacement, e.properties, e.unknown])).toEqual(
|
|
36
|
+
FIXTURE_ENTRIES.map((e) => [e.logicalId, e.action, e.replacement, e.properties, e.unknown]),
|
|
37
|
+
);
|
|
38
|
+
// terraform_data.keep is a no-op, so it has no entry.
|
|
39
|
+
expect(shape.entries.some((e) => e.logicalId === "terraform_data.keep")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("the choudoufu set plan's embedded plan gives a shape too", () => {
|
|
43
|
+
const doc = read("choudoufu-set-plan.json") as { roots: Array<{ plan?: unknown }> };
|
|
44
|
+
expect(terraformPlanShape(doc.roots[0]!.plan, "estates/e01")).toEqual({
|
|
45
|
+
member: "estates/e01",
|
|
46
|
+
entries: [{ logicalId: "terraform_data.x", action: "create", properties: [], unknown: [] }],
|
|
47
|
+
unknownInputs: [],
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a forget has an entry with no properties", () => {
|
|
52
|
+
const shape = terraformPlanShape(read("terraform.import-forget.plan.json"), "estate");
|
|
53
|
+
expect(shape.entries.find((e) => e.logicalId === "terraform_data.a")).toMatchObject({ action: "forget", properties: [], unknown: [] });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("the digest of the same plan JSON is equal across two runs", () => {
|
|
57
|
+
const one = planShapeDigest(terraformPlanShape(read("terraform.plan.json"), "estate"));
|
|
58
|
+
const two = planShapeDigest(terraformPlanShape(read("terraform.plan.json"), "estate"));
|
|
59
|
+
expect(one).toBe(two);
|
|
60
|
+
expect(planShapeDigest(terraformPlanShape(read("tofu.plan.json"), "estate"))).toBe(one);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a shape is covered by itself, and not by a plan that adds a resource", () => {
|
|
64
|
+
const first = terraformPlanShape(read("terraform.plan.json"), "estate");
|
|
65
|
+
expect(planShapeCovers(first, terraformPlanShape(read("terraform.plan.json"), "estate"))).toBeUndefined();
|
|
66
|
+
const plan = read("terraform.plan.json") as { resource_changes: unknown[] };
|
|
67
|
+
const more = { ...plan, resource_changes: [...plan.resource_changes, { address: "terraform_data.extra", type: T, change: { actions: ["create"], before: null, after: { input: "x" }, after_unknown: {} } }] };
|
|
68
|
+
expect(planShapeCovers(first, terraformPlanShape(more, "estate"))).toContain("terraform_data.extra");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("unknown and known inputs are the caller's, and enter the digest", () => {
|
|
72
|
+
const plan = read("terraform.plan.json");
|
|
73
|
+
const shape = terraformPlanShape(plan, "app", { unknownInputs: ["a.id"], knownInputs: { "net.cidr": "10.0.0.0/16" } });
|
|
74
|
+
expect(shape.unknownInputs).toEqual(["a.id"]);
|
|
75
|
+
expect(shape.knownInputs).toEqual({ "net.cidr": "10.0.0.0/16" });
|
|
76
|
+
expect(planShapeDigest(shape)).not.toBe(planShapeDigest(terraformPlanShape(plan, "app")));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** A plan with one update whose `replace_paths` and `after_unknown` are as given. */
|
|
81
|
+
function updatePlan(change: Record<string, unknown>, extra: Record<string, unknown> = {}): unknown {
|
|
82
|
+
return { resource_changes: [{ address: "aws_instance.web", type: "aws_instance", ...extra, change: { actions: ["update"], ...change } }] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("terraformPlanShape replacement", () => {
|
|
86
|
+
test("an update with a replace_paths entry replaces", () => {
|
|
87
|
+
const plan = updatePlan({ before: { ami: "a" }, after: { ami: "b" }, after_unknown: {}, replace_paths: [["ami"]] });
|
|
88
|
+
expect(terraformPlanShape(plan, "m").entries[0]).toMatchObject({ action: "update", replacement: "true", type: "aws_instance", properties: ["ami"] });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("an update whose replaced path is unknown after apply is conditional", () => {
|
|
92
|
+
const plan = updatePlan({ before: { ami: "a" }, after: {}, after_unknown: { ami: true }, replace_paths: [["ami"]] });
|
|
93
|
+
expect(terraformPlanShape(plan, "m").entries[0]).toMatchObject({ replacement: "conditional", properties: ["ami"], unknown: ["ami"] });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a nested replaced path is unknown when its own leaf is", () => {
|
|
97
|
+
const plan = updatePlan({ before: { tags: { env: "a" } }, after: { tags: {} }, after_unknown: { tags: { env: true } }, replace_paths: [["tags", "env"]] });
|
|
98
|
+
expect(terraformPlanShape(plan, "m").entries[0]!.replacement).toBe("conditional");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("one known replaced path is enough to replace", () => {
|
|
102
|
+
const plan = updatePlan({
|
|
103
|
+
before: { ami: "a", zone: "x" },
|
|
104
|
+
after: { zone: "y" },
|
|
105
|
+
after_unknown: { ami: true },
|
|
106
|
+
replace_paths: [["ami"], ["zone"]],
|
|
107
|
+
});
|
|
108
|
+
expect(terraformPlanShape(plan, "m").entries[0]!.replacement).toBe("true");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("an update with no replace_paths replaces nothing", () => {
|
|
112
|
+
const plan = updatePlan({ before: { ami: "a" }, after: { ami: "b" }, after_unknown: {} });
|
|
113
|
+
expect(terraformPlanShape(plan, "m").entries[0]!.replacement).toBe("false");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a delete and create pair with no replace_paths replaces", () => {
|
|
117
|
+
const plan = { resource_changes: [{ address: "x.y", type: "x", change: { actions: ["create", "delete"], before: { a: 1 }, after: { a: 2 }, after_unknown: {} } }] };
|
|
118
|
+
expect(terraformPlanShape(plan, "m").entries[0]).toMatchObject({ action: "replace", replacement: "true" });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("terraformPlanShape, edge cases", () => {
|
|
123
|
+
test("a deposed object's logical id carries its key", () => {
|
|
124
|
+
const plan = updatePlan({ before: { a: 1 }, after: null, actions: ["delete"] }, { deposed: "0123abcd" });
|
|
125
|
+
const [entry] = terraformPlanShape(plan, "m").entries;
|
|
126
|
+
expect(entry).toMatchObject({ logicalId: "aws_instance.web#0123abcd", action: "delete" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("read and no-op entries are left out", () => {
|
|
130
|
+
const plan = {
|
|
131
|
+
resource_changes: [
|
|
132
|
+
{ address: "data.x.a", type: "x", change: { actions: ["read"] } },
|
|
133
|
+
{ address: "x.b", type: "x", change: { actions: ["no-op"], before: {}, after: {} } },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
expect(terraformPlanShape(plan, "m").entries).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("anything that is not a plan has no entries", () => {
|
|
140
|
+
expect(terraformPlanShape(undefined, "m")).toEqual({ member: "m", entries: [], unknownInputs: [] });
|
|
141
|
+
expect(terraformPlanShape({}, "m").entries).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("sensitive values never enter the shape", () => {
|
|
145
|
+
const shape = terraformPlanShape(read("terraform.plan.json"), "estate");
|
|
146
|
+
expect(JSON.stringify(shape)).not.toContain("s3cr3t");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan shape of a terraform-family root (#4436): what `terraform show
|
|
3
|
+
* -json` says a plan does, without the values, in the form core's
|
|
4
|
+
* `coveredRound` compares (#4419). `terraform-apply`'s `plan` returns it, and
|
|
5
|
+
* terragucci calls it for the plan JSON of a note (terragucci#1268).
|
|
6
|
+
*
|
|
7
|
+
* Like `./change-set`, this module imports only `@intentius/chant` subpaths,
|
|
8
|
+
* never the lexicon's entry point.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PlanShape, PlanShapeEntry, ShapeReplacement } from "@intentius/chant/components/covered-round";
|
|
12
|
+
import { normalizePlanShape } from "@intentius/chant/components/covered-round";
|
|
13
|
+
import { actionFor } from "./change-set";
|
|
14
|
+
|
|
15
|
+
type Json = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
18
|
+
|
|
19
|
+
/** Whether an `after_unknown` value marks anything under it unknown. */
|
|
20
|
+
function marks(v: unknown): boolean {
|
|
21
|
+
if (v === true) return true;
|
|
22
|
+
if (Array.isArray(v)) return v.some(marks);
|
|
23
|
+
if (isObject(v)) return Object.values(v).some(marks);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameJson(a: unknown, b: unknown): boolean {
|
|
28
|
+
if (a === b) return true;
|
|
29
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => sameJson(x, b[i]));
|
|
30
|
+
if (isObject(a) && isObject(b)) {
|
|
31
|
+
const ka = Object.keys(a);
|
|
32
|
+
return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameJson(a[k], b[k]));
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether the value at `path` in an `after_unknown` tree is unknown: `true` at the path or on a prefix of it. */
|
|
38
|
+
function unknownAt(tree: unknown, path: unknown[]): boolean {
|
|
39
|
+
let at: unknown = tree;
|
|
40
|
+
for (const step of path) {
|
|
41
|
+
if (at === true) return true;
|
|
42
|
+
if (Array.isArray(at) && typeof step === "number") at = at[step];
|
|
43
|
+
else if (isObject(at) && typeof step === "string") at = at[step];
|
|
44
|
+
else return false;
|
|
45
|
+
}
|
|
46
|
+
return marks(at);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How a change replaces its resource. A `replace_paths` entry names an
|
|
51
|
+
* attribute whose change forces it. When every such attribute is unknown
|
|
52
|
+
* after apply, the plan cannot say the value will differ, so the replacement
|
|
53
|
+
* is `conditional`; one with a known forcing attribute is `true`. A delete
|
|
54
|
+
* and create pair with no `replace_paths` (a tainted object, `-replace`) is
|
|
55
|
+
* `true`. An update with none replaces nothing (`false`); a create or delete
|
|
56
|
+
* has no replacement to state.
|
|
57
|
+
*/
|
|
58
|
+
function replacementFor(change: Json, action: string): ShapeReplacement | undefined {
|
|
59
|
+
const paths = (Array.isArray(change.replace_paths) ? change.replace_paths : []).filter(Array.isArray) as unknown[][];
|
|
60
|
+
if (paths.length > 0) return paths.every((p) => unknownAt(change.after_unknown, p)) ? "conditional" : "true";
|
|
61
|
+
if (action === "replace") return "true";
|
|
62
|
+
if (action === "update") return "false";
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TerraformPlanShapeOptions {
|
|
67
|
+
/** The inputs the plan was made without, as `<component>.<output>`. */
|
|
68
|
+
unknownInputs?: string[];
|
|
69
|
+
/** The inputs the plan read with a value, by reference. */
|
|
70
|
+
knownInputs?: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The shape of `planJson` (`terraform show -json`, from terraform, tofu or
|
|
75
|
+
* choudoufu) for `member`.
|
|
76
|
+
*
|
|
77
|
+
* One entry per `resource_changes` item that is not `no-op` or `read`.
|
|
78
|
+
* `logicalId` is the address, with `#<deposed>` for a deposed object. `type`
|
|
79
|
+
* is the resource type. `properties` are the top-level attributes that differ
|
|
80
|
+
* between `before` and `after` (on a create, those the plan sets or leaves
|
|
81
|
+
* unknown), and `unknown` the ones `after_unknown` marks, so `unknown` is
|
|
82
|
+
* within `properties`. A delete has no properties. Values never enter it, so
|
|
83
|
+
* a sensitive attribute is only a name.
|
|
84
|
+
*/
|
|
85
|
+
export function terraformPlanShape(planJson: unknown, member: string, options: TerraformPlanShapeOptions = {}): PlanShape {
|
|
86
|
+
const plan = isObject(planJson) ? planJson : {};
|
|
87
|
+
const raw = Array.isArray(plan.resource_changes) ? plan.resource_changes : [];
|
|
88
|
+
const entries: PlanShapeEntry[] = [];
|
|
89
|
+
for (const r of raw) {
|
|
90
|
+
const rc = isObject(r) ? r : {};
|
|
91
|
+
const change = isObject(rc.change) ? rc.change : {};
|
|
92
|
+
const action = actionFor(change.actions);
|
|
93
|
+
if (action === "no-op" || action === "read") continue;
|
|
94
|
+
const address = String(rc.address ?? "");
|
|
95
|
+
const before = isObject(change.before) ? change.before : {};
|
|
96
|
+
const after = isObject(change.after) ? change.after : {};
|
|
97
|
+
const afterUnknown = isObject(change.after_unknown) ? change.after_unknown : {};
|
|
98
|
+
const properties: string[] = [];
|
|
99
|
+
const unknown: string[] = [];
|
|
100
|
+
if (action !== "delete" && action !== "forget") {
|
|
101
|
+
for (const k of new Set([...Object.keys(before), ...Object.keys(after), ...Object.keys(afterUnknown)])) {
|
|
102
|
+
const isUnknown = marks(afterUnknown[k]);
|
|
103
|
+
const differs =
|
|
104
|
+
action === "create"
|
|
105
|
+
? isUnknown || (after[k] !== undefined && after[k] !== null)
|
|
106
|
+
: isUnknown || !sameJson(before[k] ?? null, after[k] ?? null);
|
|
107
|
+
if (!differs) continue;
|
|
108
|
+
properties.push(k);
|
|
109
|
+
if (isUnknown) unknown.push(k);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const replacement = replacementFor(change, action);
|
|
113
|
+
entries.push({
|
|
114
|
+
logicalId: typeof rc.deposed === "string" ? `${address}#${rc.deposed}` : address,
|
|
115
|
+
action,
|
|
116
|
+
...(replacement ? { replacement } : {}),
|
|
117
|
+
...(typeof rc.type === "string" ? { type: rc.type } : {}),
|
|
118
|
+
properties,
|
|
119
|
+
unknown,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return normalizePlanShape({
|
|
123
|
+
member,
|
|
124
|
+
entries,
|
|
125
|
+
unknownInputs: options.unknownInputs ?? [],
|
|
126
|
+
...(options.knownInputs ? { knownInputs: options.knownInputs } : {}),
|
|
127
|
+
});
|
|
128
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { lastMemberReport, readPulumiMember, setLastMemberReport } from "./workspace-member";
|
|
2
1
|
import type { AuditEntitiesInput, LexiconPlugin } from "@intentius/chant/lexicon";
|
|
3
2
|
import { terraformBehaviourKinds } from "./behaviour/kinds";
|
|
4
3
|
import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
|
|
@@ -11,7 +10,7 @@ import { withCallersChain } from "./lint/post-synth/scope";
|
|
|
11
10
|
import { terraformAuditCatalog } from "./lint/audit-catalog";
|
|
12
11
|
import { completions } from "./lsp/completions";
|
|
13
12
|
import { hover } from "./lsp/hover";
|
|
14
|
-
import { terraformConfigSchema, type TerraformConfig } from "./config";
|
|
13
|
+
import { ownRoots, terraformConfigSchema, type TerraformConfig } from "./config";
|
|
15
14
|
import { renderTerraformRoots } from "./hcl/roots";
|
|
16
15
|
import { existsSync } from "node:fs";
|
|
17
16
|
import { join } from "node:path";
|
|
@@ -20,6 +19,7 @@ import { descendModules } from "./hcl/descend";
|
|
|
20
19
|
import { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
|
|
21
20
|
import { terraformCommands } from "./commands";
|
|
22
21
|
import { terraformCapabilityPack } from "./capabilities/adapter";
|
|
22
|
+
import { TERRAFORM_REFERENCE_CATALOG } from "./behaviour/reference-catalog";
|
|
23
23
|
|
|
24
24
|
const loadSkills = createSkillsLoader(import.meta.url, [
|
|
25
25
|
{
|
|
@@ -79,6 +79,8 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
79
79
|
behaviourKinds: terraformBehaviourKinds,
|
|
80
80
|
name: "terraform",
|
|
81
81
|
capabilityPack: terraformCapabilityPack,
|
|
82
|
+
// Keyed by provider type (#2360); see its module doc for which nodes it reads (#4535).
|
|
83
|
+
referenceCatalog: TERRAFORM_REFERENCE_CATALOG,
|
|
82
84
|
serializer: terraformSerializer,
|
|
83
85
|
configSchema: terraformConfigSchema,
|
|
84
86
|
|
|
@@ -141,6 +143,18 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
141
143
|
return describeResources(options);
|
|
142
144
|
},
|
|
143
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Each root's drift, as drift-record stream lines (behold#995): a stock
|
|
148
|
+
* root through its state or a plan, a live root through `live-plan` (and a
|
|
149
|
+
* plan), one resource through `live-plan -resource`. `chant workspace
|
|
150
|
+
* drift` reaches it through `chant lifecycle drift` in a member's reader
|
|
151
|
+
* project. See `./drift-read.ts`.
|
|
152
|
+
*/
|
|
153
|
+
async readDrift(options) {
|
|
154
|
+
const { readTerraformDrift } = await import("./drift-read");
|
|
155
|
+
return readTerraformDrift(options);
|
|
156
|
+
},
|
|
157
|
+
|
|
144
158
|
/**
|
|
145
159
|
* The prediction, for whichever estate `options.from` names (#2495). The
|
|
146
160
|
* producer is `predictTerraformBehaviour` and this is its second caller, not
|
|
@@ -181,15 +195,9 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
181
195
|
* project root, which is what relative `dir` paths resolve against.
|
|
182
196
|
*/
|
|
183
197
|
async buildRoots(ctx): Promise<{ entities: Map<string, Declarable>; warnings: string[] }> {
|
|
184
|
-
setLastMemberReport(undefined);
|
|
185
198
|
const namespace = (ctx.config as { terraform?: TerraformConfig }).terraform;
|
|
186
|
-
// A
|
|
187
|
-
|
|
188
|
-
const read = readPulumiMember({ member: namespace.pulumiMember });
|
|
189
|
-
setLastMemberReport(read.report);
|
|
190
|
-
return { entities: read.entities, warnings: read.warnings };
|
|
191
|
-
}
|
|
192
|
-
const roots = namespace?.roots ?? {};
|
|
199
|
+
// A `signalOnly` root (#4484) feeds change detection only, so it builds no entities here.
|
|
200
|
+
const roots = ownRoots(namespace?.roots);
|
|
193
201
|
if (Object.keys(roots).length === 0) return { entities: new Map(), warnings: [] };
|
|
194
202
|
return renderTerraformRoots({
|
|
195
203
|
projectRoot: ctx.projectRoot,
|
|
@@ -200,11 +208,6 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
200
208
|
});
|
|
201
209
|
},
|
|
202
210
|
|
|
203
|
-
graphMeta() {
|
|
204
|
-
const report = lastMemberReport();
|
|
205
|
-
return report ? { pulumiMember: report } : undefined;
|
|
206
|
-
},
|
|
207
|
-
|
|
208
211
|
/** `chant terraform pin-rollout` (#3189). */
|
|
209
212
|
commands() {
|
|
210
213
|
return terraformCommands;
|
|
@@ -213,13 +216,15 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
213
216
|
/**
|
|
214
217
|
* The `terraform.roots` a change touched (#3183): a changed file in the
|
|
215
218
|
* root's directory, in a local module it calls, or one of its var files.
|
|
216
|
-
* See `./changed-roots.ts`.
|
|
219
|
+
* See `./changed-roots.ts`. Every root counts, a `signalOnly` one too
|
|
220
|
+
* (#4484): that is what it is declared for.
|
|
217
221
|
*/
|
|
218
222
|
async changedUnits(ctx) {
|
|
219
|
-
const
|
|
223
|
+
const tf = (ctx.config as { terraform?: TerraformConfig }).terraform;
|
|
224
|
+
const roots = tf?.roots ?? {};
|
|
220
225
|
if (Object.keys(roots).length === 0) return [];
|
|
221
226
|
const { changedRoots } = await import("./changed-roots");
|
|
222
|
-
return changedRoots(ctx.projectRoot, roots, ctx.changedFiles);
|
|
227
|
+
return changedRoots(ctx.projectRoot, roots, ctx.changedFiles, tf?.binary);
|
|
223
228
|
},
|
|
224
229
|
|
|
225
230
|
lintRules() {
|
|
@@ -294,12 +299,21 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
294
299
|
// A local audit knows the root's directory, so it can say whether the
|
|
295
300
|
// lock file is beside the `.tf` files (TF040); a fetched one cannot.
|
|
296
301
|
const lockFile = input?.dir !== undefined ? existsSync(join(input.dir, LOCK_FILENAME)) : undefined;
|
|
297
|
-
|
|
302
|
+
// An audit has no `terraform.binary` to follow (chant#4457). A root
|
|
303
|
+
// that carries a `.tofu` file is written for OpenTofu, so it is read
|
|
304
|
+
// as a tofu root; any other is read as terraform, the config default.
|
|
305
|
+
const binary = /^# file: .*\.tofu(\.json)?\s*$/m.test(content) ? "tofu" : undefined;
|
|
306
|
+
const modeOptions = {
|
|
307
|
+
...(binary !== undefined ? { binary } : {}),
|
|
308
|
+
...(lockFile !== undefined ? { lockFile } : {}),
|
|
309
|
+
};
|
|
310
|
+
const entities = await parseTerraformRootContent(content, root, undefined, Object.keys(modeOptions).length > 0 ? modeOptions : undefined);
|
|
298
311
|
if (input?.dir === undefined) return entities;
|
|
299
312
|
const { entities: children } = await descendModules(entities, {
|
|
300
313
|
dir: input.dir,
|
|
301
314
|
root,
|
|
302
315
|
projectRoot: input.baseDir ?? input.dir,
|
|
316
|
+
...(binary !== undefined ? { modeOptions: { binary } } : {}),
|
|
303
317
|
});
|
|
304
318
|
for (const [key, entity] of children) entities.set(key, entity);
|
|
305
319
|
return entities;
|
package/src/report.test.ts
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { contract } from "../../../packages/core/src/workspace/__fixtures__/contract-repo";
|
|
11
12
|
import { describe, expect, test } from "vitest";
|
|
13
|
+
import { verifyChangeSetDigest } from "@intentius/chant/change-set";
|
|
12
14
|
import { addToIndex, atmosOutputReads, atmosStackReads, dirStore, estateOfDir, httpStore, moduleEstates, runFactsFromEnv, slicePlan, terraformReport, topModule, writeReport } from "./report";
|
|
13
15
|
import { parseReportArgs } from "./commands";
|
|
16
|
+
import { generateReportFixture } from "./__fixtures__/report/generate";
|
|
17
|
+
import { readdirSync } from "node:fs";
|
|
14
18
|
|
|
15
19
|
const rc = (address: string, module: string, type: string, name: string, actions: string[], change: Record<string, unknown>) => ({
|
|
16
20
|
address, module_address: module, mode: "managed", type, name, provider_name: "registry.terraform.io/hashicorp/aws", change: { actions, ...change },
|
|
@@ -287,7 +291,7 @@ describe("writing", () => {
|
|
|
287
291
|
const store = dirStore(dir);
|
|
288
292
|
const plan = terraformReport({ ...base, stage: "tf-plan" });
|
|
289
293
|
const apply = terraformReport({ ...base, stage: "tf-apply", applied: AFTER, finished: "2026-10-24T10:05:00Z" });
|
|
290
|
-
expect(await writeReport(store, plan)).toEqual([`${RUN.project}/2026/10/abc123/tf-plan/report.json`, `${RUN.project}/index.json`, "index.json"]);
|
|
294
|
+
expect(await writeReport(store, plan)).toEqual([`${RUN.project}/2026/10/abc123/tf-plan/report.json`, `${RUN.project}/2026/10/abc123/tf-plan/change-set.json`, `${RUN.project}/index.json`, "index.json"]);
|
|
291
295
|
await writeReport(store, apply);
|
|
292
296
|
await writeReport(store, apply);
|
|
293
297
|
const project = JSON.parse(readFileSync(join(dir, RUN.project, "index.json"), "utf8"));
|
|
@@ -322,6 +326,83 @@ describe("writing", () => {
|
|
|
322
326
|
});
|
|
323
327
|
});
|
|
324
328
|
|
|
329
|
+
const SCHEMA_PATH = join(import.meta.dirname, "..", "..", "..", "packages", "core", "src", "workspace", "change-set.schema.json");
|
|
330
|
+
const { validate: validateChangeSet } = contract(JSON.parse(readFileSync(SCHEMA_PATH, "utf8")));
|
|
331
|
+
const FIXTURE_DIR = join(import.meta.dirname, "__fixtures__", "report");
|
|
332
|
+
|
|
333
|
+
/** The change set must validate against chant's schema and be the document the report's digest names. */
|
|
334
|
+
function expectChangeSet(doc: any, report: any): void {
|
|
335
|
+
expect(validateChangeSet(doc), JSON.stringify(validateChangeSet.errors, null, 2)).toBe(true);
|
|
336
|
+
expect(verifyChangeSetDigest(doc)).toBe(true);
|
|
337
|
+
expect(report.change_set).toBe(doc.digest);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
describe("change-set.json beside the report", () => {
|
|
341
|
+
const apply = { ...base, stage: "tf-apply" as const, applied: AFTER, finished: "2026-10-24T10:05:00Z" };
|
|
342
|
+
|
|
343
|
+
test.each([["tf-plan", { ...base, stage: "tf-plan" as const }], ["tf-apply", apply]])("%s: the document validates and its digest is the report's", (_stage, input) => {
|
|
344
|
+
const r = terraformReport(input);
|
|
345
|
+
expectChangeSet(r.changeSet, r.report);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("--out writes it next to report.json for both stages", async () => {
|
|
349
|
+
const dir = mkdtempSync(join(tmpdir(), "tf-report-cs-"));
|
|
350
|
+
const store = dirStore(dir);
|
|
351
|
+
for (const input of [{ ...base, stage: "tf-plan" as const }, apply]) {
|
|
352
|
+
const r = terraformReport(input);
|
|
353
|
+
await writeReport(store, r);
|
|
354
|
+
const at = join(dir, RUN.project, r.path);
|
|
355
|
+
expect(readdirSync(at).sort()).toEqual(["change-set.json", "report.json"]);
|
|
356
|
+
expectChangeSet(JSON.parse(readFileSync(join(at, "change-set.json"), "utf8")), JSON.parse(readFileSync(join(at, "report.json"), "utf8")));
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("--to puts it next to report.json for both stages", async () => {
|
|
361
|
+
const objects = new Map<string, string>();
|
|
362
|
+
const fake = (async (url: string, init?: RequestInit) => {
|
|
363
|
+
if (init?.method === "PUT") {
|
|
364
|
+
objects.set(url, String(init.body));
|
|
365
|
+
return new Response("", { status: 200 });
|
|
366
|
+
}
|
|
367
|
+
return objects.has(url) ? new Response(objects.get(url)!) : new Response("", { status: 404 });
|
|
368
|
+
}) as unknown as typeof fetch;
|
|
369
|
+
const store = httpStore("http://floci:4566/demo-reports/demo", fake);
|
|
370
|
+
for (const input of [{ ...base, stage: "tf-plan" as const }, apply]) {
|
|
371
|
+
const r = terraformReport(input);
|
|
372
|
+
await writeReport(store, r);
|
|
373
|
+
const at = `http://floci:4566/demo-reports/demo/${encodeURIComponent("forgejo:3000").replace(/%3A/g, "%3A")}/admin/demo-terragrunt-choudoufu/${r.path}`;
|
|
374
|
+
expectChangeSet(JSON.parse(objects.get(`${at}/change-set.json`)!), JSON.parse(objects.get(`${at}/report.json`)!));
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("the report fixture behold renders", () => {
|
|
380
|
+
const read = (name: string): any => JSON.parse(readFileSync(join(FIXTURE_DIR, name), "utf8"));
|
|
381
|
+
|
|
382
|
+
test("is what the generator writes now; regenerate with `npx tsx lexicons/terraform/src/__fixtures__/report/generate.ts`", async () => {
|
|
383
|
+
const files = await generateReportFixture();
|
|
384
|
+
expect(Object.keys(files).sort()).toEqual(["tf-apply/change-set.json", "tf-apply/report.json", "tf-plan/change-set.json", "tf-plan/report.json"]);
|
|
385
|
+
for (const [name, body] of Object.entries(files)) expect(readFileSync(join(FIXTURE_DIR, name), "utf8"), `${name} is stale`).toBe(body);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("both stages validate, and each digest is its report's", () => {
|
|
389
|
+
for (const stage of ["tf-plan", "tf-apply"]) expectChangeSet(read(`${stage}/change-set.json`), read(`${stage}/report.json`));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("three members, two composed reads", () => {
|
|
393
|
+
const plan = read("tf-plan/report.json");
|
|
394
|
+
expect(read("tf-plan/change-set.json").members.map((m: any) => m.member)).toEqual(["estates/app", "estates/edge", "estates/network"]);
|
|
395
|
+
const reads = plan.roots.flatMap((r: any) => r.reads ?? []);
|
|
396
|
+
expect(reads.filter((r: any) => r.composed === true).length).toBeGreaterThanOrEqual(2);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("an attribute unknown at plan has its value after the apply", () => {
|
|
400
|
+
const attr = (stage: string) => read(`${stage}/report.json`).roots.find((r: any) => r.path === "estates/network").changes.find((c: any) => c.address === "aws_subnet.new").attributes.find((a: any) => a.path === "id");
|
|
401
|
+
expect(attr("tf-plan")).toEqual({ path: "id", unknown: true });
|
|
402
|
+
expect(attr("tf-apply")).toEqual({ path: "id", after: "subnet-0a1b" });
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
325
406
|
describe("the run's facts and the verb's flags", () => {
|
|
326
407
|
test("a Forgejo environment names the project as terragucci does", () => {
|
|
327
408
|
const f = runFactsFromEnv({ GITHUB_REPOSITORY: "admin/demo", GITHUB_SERVER_URL: "http://forgejo:3000", GITHUB_SHA: "deadbeef", GITHUB_RUN_ID: "9", FORGEJO_ACTIONS: "true" }, { pullRequest: "12" });
|