@intentius/chant-lexicon-terraform 0.124.51 → 0.124.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
- package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
- package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
- package/dist/__fixtures__/report/generate.d.ts +26 -0
- package/dist/__fixtures__/report/generate.d.ts.map +1 -0
- package/dist/__fixtures__/report/generate.js +58 -0
- package/dist/applied-state.d.ts +27 -0
- package/dist/applied-state.d.ts.map +1 -0
- package/dist/applied-state.js +44 -0
- package/dist/backend-config.d.ts +53 -0
- package/dist/backend-config.d.ts.map +1 -0
- package/dist/backend-config.js +136 -0
- package/dist/behaviour/kinds.d.ts.map +1 -1
- package/dist/behaviour/kinds.js +4 -0
- package/dist/behaviour/predict.d.ts.map +1 -1
- package/dist/behaviour/predict.js +6 -2
- package/dist/behaviour/reference-catalog.d.ts +16 -5
- package/dist/behaviour/reference-catalog.d.ts.map +1 -1
- package/dist/behaviour/reference-catalog.js +16 -5
- package/dist/behaviour/request.d.ts.map +1 -1
- package/dist/behaviour/request.js +3 -2
- package/dist/changed-roots.d.ts +6 -4
- package/dist/changed-roots.d.ts.map +1 -1
- package/dist/changed-roots.js +14 -18
- package/dist/commands.d.ts +5 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +87 -4
- package/dist/components/capability-plugin.d.ts +65 -3
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +197 -13
- package/dist/components/compose-group.d.ts +145 -0
- package/dist/components/compose-group.d.ts.map +1 -0
- package/dist/components/compose-group.js +407 -0
- package/dist/components/placeholder-preview.d.ts +78 -0
- package/dist/components/placeholder-preview.d.ts.map +1 -0
- package/dist/components/placeholder-preview.js +254 -0
- package/dist/compose/apply-events.d.ts +46 -0
- package/dist/compose/apply-events.d.ts.map +1 -0
- package/dist/compose/apply-events.js +77 -0
- package/dist/compose/estate.d.ts +194 -0
- package/dist/compose/estate.d.ts.map +1 -0
- package/dist/compose/estate.js +363 -0
- package/dist/compose/hcl.d.ts +99 -0
- package/dist/compose/hcl.d.ts.map +1 -0
- package/dist/compose/hcl.js +302 -0
- package/dist/compose/module-address.d.ts +24 -0
- package/dist/compose/module-address.d.ts.map +1 -0
- package/dist/compose/module-address.js +43 -0
- package/dist/compose/root.d.ts +127 -0
- package/dist/compose/root.d.ts.map +1 -0
- package/dist/compose/root.js +586 -0
- package/dist/compose/run.d.ts +183 -0
- package/dist/compose/run.d.ts.map +1 -0
- package/dist/compose/run.js +447 -0
- package/dist/compose/slice.d.ts +102 -0
- package/dist/compose/slice.d.ts.map +1 -0
- package/dist/compose/slice.js +249 -0
- package/dist/compose/state-address.d.ts +28 -0
- package/dist/compose/state-address.d.ts.map +1 -0
- package/dist/compose/state-address.js +154 -0
- package/dist/compose/state-merge.d.ts +52 -0
- package/dist/compose/state-merge.d.ts.map +1 -0
- package/dist/compose/state-merge.js +135 -0
- package/dist/compose.d.ts +179 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +482 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +7 -3
- package/dist/config.d.ts +59 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/describe-resources.d.ts +74 -12
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +374 -66
- package/dist/drift-read.d.ts +33 -0
- package/dist/drift-read.d.ts.map +1 -0
- package/dist/drift-read.js +243 -0
- package/dist/drift-record.d.ts +147 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +383 -0
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/edges.js +24 -1
- package/dist/hcl/files.d.ts +61 -0
- package/dist/hcl/files.d.ts.map +1 -0
- package/dist/hcl/files.js +221 -0
- package/dist/hcl/instances.d.ts +82 -0
- package/dist/hcl/instances.d.ts.map +1 -0
- package/dist/hcl/instances.js +390 -0
- package/dist/hcl/parse.d.ts +81 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +114 -16
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/roots.js +5 -0
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/hcl/suppressions.js +8 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/integrity.json +3 -3
- package/dist/live-plan-reasons.d.ts +21 -0
- package/dist/live-plan-reasons.d.ts.map +1 -0
- package/dist/live-plan-reasons.js +47 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/lsp/option-keys.js +2 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +6 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +5 -1
- package/dist/op/activities/live-detect.d.ts +4 -3
- package/dist/op/activities/live-detect.d.ts.map +1 -1
- package/dist/op/activities/live-detect.js +57 -32
- package/dist/op/activities/pin-rollout.d.ts +2 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.js +1 -0
- package/dist/op/activities/terraform.d.ts +153 -5
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +205 -20
- package/dist/op/activity-contracts.d.ts +27 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +19 -1
- package/dist/op/builders.d.ts +7 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -0
- package/dist/pin/edit.d.ts.map +1 -1
- package/dist/pin/edit.js +35 -0
- package/dist/pin/rollout.d.ts +2 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +14 -11
- package/dist/plan-shape.d.ts +30 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +125 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -18
- package/dist/report.d.ts +216 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +666 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/state/azure-blob.d.ts +68 -0
- package/dist/state/azure-blob.d.ts.map +1 -0
- package/dist/state/azure-blob.js +201 -0
- package/dist/state/backend.d.ts +158 -0
- package/dist/state/backend.d.ts.map +1 -0
- package/dist/state/backend.js +406 -0
- package/dist/state/exec.d.ts +39 -0
- package/dist/state/exec.d.ts.map +1 -0
- package/dist/state/exec.js +80 -0
- package/dist/state/gcs.d.ts +62 -0
- package/dist/state/gcs.d.ts.map +1 -0
- package/dist/state/gcs.js +224 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +14 -0
- package/dist/state/locks.d.ts +105 -0
- package/dist/state/locks.d.ts.map +1 -0
- package/dist/state/locks.js +130 -0
- package/dist/state/object-store.d.ts +37 -0
- package/dist/state/object-store.d.ts.map +1 -0
- package/dist/state/object-store.js +20 -0
- package/dist/state/root-states.d.ts +131 -0
- package/dist/state/root-states.d.ts.map +1 -0
- package/dist/state/root-states.js +241 -0
- package/dist/state/s3.d.ts +68 -0
- package/dist/state/s3.d.ts.map +1 -0
- package/dist/state/s3.js +170 -0
- package/dist/state/store.d.ts +51 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +185 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +2 -1
- package/package.json +7 -3
- package/src/__fixtures__/ACCEPTANCE.md +69 -7
- package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
- package/src/__fixtures__/compose/state-app.json +66 -0
- package/src/__fixtures__/compose/state-edge.json +40 -0
- package/src/__fixtures__/compose/state-net.json +58 -0
- package/src/__fixtures__/compose/three-app.plan.json +184 -0
- package/src/__fixtures__/compose/three-composed.plan.json +509 -0
- package/src/__fixtures__/compose/three-edge.plan.json +144 -0
- package/src/__fixtures__/compose/three-net.plan.json +174 -0
- package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
- package/src/__fixtures__/compose/working-after-apply.json +145 -0
- package/src/__fixtures__/config-blocks/app/main.tf +61 -0
- package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
- package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
- package/src/__fixtures__/config-files/shadow/main.tf +3 -0
- package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
- package/src/__fixtures__/config-files/shadow/other.tf +3 -0
- package/src/__fixtures__/config-files/tf/main.tf +11 -0
- package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
- package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
- package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
- package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
- package/src/__fixtures__/drift-record/README.md +11 -0
- package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
- package/src/__fixtures__/instance-forms/app/main.tf +110 -0
- package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/main.tf +52 -0
- package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
- package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
- package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
- package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
- package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
- package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
- package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
- package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
- package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
- package/src/__fixtures__/pr-loop/README.md +50 -0
- package/src/__fixtures__/pr-loop/chant.config.json +13 -0
- package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
- package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
- package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
- package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
- package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
- package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
- package/src/__fixtures__/report/applied.show.json +51 -0
- package/src/__fixtures__/report/generate.ts +63 -0
- package/src/__fixtures__/report/plan.show.json +200 -0
- package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
- package/src/__fixtures__/report/tf-apply/report.json +338 -0
- package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
- package/src/__fixtures__/report/tf-plan/report.json +320 -0
- package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
- package/src/applied-state.ts +52 -0
- package/src/backend-config.test.ts +105 -0
- package/src/backend-config.ts +133 -0
- package/src/behaviour/kinds.ts +4 -0
- package/src/behaviour/predict.ts +5 -2
- package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
- package/src/behaviour/reference-catalog.ts +16 -5
- package/src/behaviour/request.ts +3 -2
- package/src/change-set-instances.test.ts +98 -0
- package/src/changed-roots.ts +14 -16
- package/src/commands.ts +84 -4
- package/src/components/capability-plugin.test.ts +172 -5
- package/src/components/capability-plugin.ts +212 -11
- package/src/components/change-set-graph.e2e.test.ts +11 -5
- package/src/components/compose-group.test.ts +486 -0
- package/src/components/compose-group.ts +489 -0
- package/src/components/demo-composed-roots.e2e.test.ts +440 -0
- package/src/components/member-outputs.test.ts +76 -0
- package/src/components/placeholder-preview.test.ts +147 -0
- package/src/components/placeholder-preview.ts +257 -0
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
- package/src/components/pr-loop.tofu.test.ts +112 -56
- package/src/compose/apply-events.test.ts +62 -0
- package/src/compose/apply-events.ts +100 -0
- package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
- package/src/compose/estate.test.ts +421 -0
- package/src/compose/estate.ts +443 -0
- package/src/compose/hcl.ts +311 -0
- package/src/compose/module-address.ts +49 -0
- package/src/compose/root.test.ts +416 -0
- package/src/compose/root.ts +592 -0
- package/src/compose/run.test.ts +440 -0
- package/src/compose/run.tofu.test.ts +176 -0
- package/src/compose/run.ts +527 -0
- package/src/compose/slice.test.ts +184 -0
- package/src/compose/slice.ts +290 -0
- package/src/compose/state-address.ts +157 -0
- package/src/compose/state-merge.test.ts +131 -0
- package/src/compose/state-merge.ts +148 -0
- package/src/compose.test.ts +248 -0
- package/src/compose.ts +627 -0
- package/src/composites/terraform-apply-op.test.ts +9 -0
- package/src/composites/terraform-apply-op.ts +7 -3
- package/src/composites/terraform-pin-rollout-op.test.ts +2 -2
- package/src/config-file-listers.test.ts +265 -0
- package/src/config.ts +44 -9
- package/src/describe-resources.engine-facts.test.ts +412 -0
- package/src/describe-resources.live-module-keyed.test.ts +290 -0
- package/src/describe-resources.ts +416 -70
- package/src/drift-read.ts +284 -0
- package/src/drift-record.test.ts +223 -0
- package/src/drift-record.ts +455 -0
- package/src/hcl/config-blocks.test.ts +174 -0
- package/src/hcl/edges.ts +21 -1
- package/src/hcl/files.ts +234 -0
- package/src/hcl/instance-forms.test.ts +94 -0
- package/src/hcl/instances.test.ts +137 -0
- package/src/hcl/instances.ts +422 -0
- package/src/hcl/parse.test.ts +139 -1
- package/src/hcl/parse.ts +149 -16
- package/src/hcl/roots.ts +6 -0
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +76 -0
- package/src/live-plan-reasons.ts +54 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/option-keys.ts +2 -0
- package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
- package/src/op/activities/index.ts +9 -0
- package/src/op/activities/live-detect.ts +55 -26
- package/src/op/activities/pin-rollout.ts +3 -0
- package/src/op/activities/registry.test.ts +2 -1
- package/src/op/activities/terraform.test.ts +55 -0
- package/src/op/activities/terraform.ts +266 -22
- package/src/op/activity-contracts.test.ts +1 -0
- package/src/op/activity-contracts.ts +24 -1
- package/src/op/builders.ts +14 -0
- package/src/pin/bundle.test.ts +2 -0
- package/src/pin/edit.ts +30 -0
- package/src/pin/rollout.ts +16 -11
- package/src/plan-shape.test.ts +148 -0
- package/src/plan-shape.ts +128 -0
- package/src/plugin.ts +33 -19
- package/src/report.test.ts +423 -0
- package/src/report.ts +752 -0
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +223 -0
- package/src/state/backend-verdict.test.ts +187 -0
- package/src/state/backend.test.ts +127 -0
- package/src/state/backend.ts +488 -0
- package/src/state/exec.ts +111 -0
- package/src/state/gcs.ts +254 -0
- package/src/state/index.ts +15 -0
- package/src/state/locks.test.ts +84 -0
- package/src/state/locks.ts +201 -0
- package/src/state/object-store.ts +35 -0
- package/src/state/root-states-floci.e2e.test.ts +66 -0
- package/src/state/root-states.test.ts +462 -0
- package/src/state/root-states.ts +309 -0
- package/src/state/s3.ts +193 -0
- package/src/state/store.ts +225 -0
- package/src/terragrunt/affected.ts +2 -1
- package/src/workspace-kinds.test.ts +4 -17
- package/workspace-kinds.json +1 -24
- package/dist/workspace-member.d.ts +0 -30
- package/dist/workspace-member.d.ts.map +0 -1
- package/dist/workspace-member.js +0 -91
- package/schemas/pulumi.schema.json +0 -34
- package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
- package/src/testdata/pulumi-member/stack-export.json +0 -53
- package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
- package/src/workspace-member.test.ts +0 -89
- package/src/workspace-member.ts +0 -126
|
@@ -16,11 +16,22 @@
|
|
|
16
16
|
* Every terraform `resource` block is `Terraform::Resource` to chant. A rule
|
|
17
17
|
* keyed on that would apply to a VPC and an instance alike, so the nodes
|
|
18
18
|
* `./request.ts` builds for the resolver carry the **provider type** as their
|
|
19
|
-
* `kind` — `aws_subnet` — and the rules here are keyed the same way.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
19
|
+
* `kind` — `aws_subnet` — and the rules here are keyed the same way.
|
|
20
|
+
*
|
|
21
|
+
* ## On the plugin (#4535)
|
|
22
|
+
*
|
|
23
|
+
* The plugin declares it as `referenceCatalog` since #4535, so a reader that
|
|
24
|
+
* asks every lexicon of a graph for its catalog (behold#1030's gate, before
|
|
25
|
+
* behold#1005 retires behold's name matcher) finds terraform's. What it
|
|
26
|
+
* matches depends on the nodes. A declared or live `chant graph` node is
|
|
27
|
+
* `Terraform::Resource`, with its provider type in `attrs.type` and no
|
|
28
|
+
* arguments in `attrs`, so no rule here reads it: those graphs keep the edges
|
|
29
|
+
* the HCL references and the state dependencies give them, and gain none. A
|
|
30
|
+
* node whose `kind` is the provider type and whose `attrs` are the block's
|
|
31
|
+
* arguments (`./request.ts`'s, an engine's instance roster) is what the rules
|
|
32
|
+
* are for. `chant graph --live` merges a catalog's edges with the
|
|
33
|
+
* observation's own since #4535; before, any catalog replaced them, which is
|
|
34
|
+
* what kept this one off the plugin.
|
|
24
35
|
*
|
|
25
36
|
* ## Two relations, and why the split matters to an engine
|
|
26
37
|
*
|
package/src/behaviour/request.ts
CHANGED
|
@@ -329,8 +329,9 @@ function liveVerdictOf(address: string, plan: LivePlanIndex): LiveVerdict {
|
|
|
329
329
|
kind: "unobserved",
|
|
330
330
|
instances,
|
|
331
331
|
// `no-credentials` is an observation reason and not a prediction one;
|
|
332
|
-
// the credential wording stays in the detail.
|
|
333
|
-
|
|
332
|
+
// the credential wording stays in the detail. `not-reached` is a
|
|
333
|
+
// stream's reason (#4451) and a live-plan omission never gives it.
|
|
334
|
+
reason: unobserved.reason === "no-credentials" || unobserved.reason === "not-reached" ? "read-failed" : unobserved.reason,
|
|
334
335
|
detail: unobserved.detail,
|
|
335
336
|
};
|
|
336
337
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `count = 3` resource's stored change set carries three node ids (#4454).
|
|
3
|
+
*
|
|
4
|
+
* The graph is read in process the way `chant graph --format ir` builds it
|
|
5
|
+
* for a terraform root: the root's entities, `buildGraphIr`, then
|
|
6
|
+
* `withInstanceNodes`. The plan is a `show -json` shape with one
|
|
7
|
+
* `resource_changes` row per instance, as terraform prints a `count` block.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { composeChangeSet, verifyChangeSetDigest } from "@intentius/chant/change-set";
|
|
12
|
+
import { graphNodeFor, mapChangeSetToGraph } from "@intentius/chant/change-set-graph";
|
|
13
|
+
import { buildGraphIr, withInstanceNodes } from "@intentius/chant/graph-ir";
|
|
14
|
+
import { terraformChangeSetPart } from "./change-set";
|
|
15
|
+
import { literalInstanceKeys, parseTerraformRootContent } from "./hcl/parse";
|
|
16
|
+
|
|
17
|
+
const SOURCE = `resource "aws_eip" "pool" {
|
|
18
|
+
count = 3
|
|
19
|
+
domain = "vpc"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resource "aws_s3_bucket" "logs" {
|
|
23
|
+
bucket = "logs"
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const create = (address: string, index?: number) => ({
|
|
28
|
+
address,
|
|
29
|
+
mode: "managed",
|
|
30
|
+
type: address.split(".")[0],
|
|
31
|
+
name: address.split(".")[1]!.replace(/\[.*$/, ""),
|
|
32
|
+
...(index !== undefined ? { index } : {}),
|
|
33
|
+
change: { actions: ["create"], before: null, after: { domain: "vpc" }, after_unknown: { id: true } },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const PLAN = {
|
|
37
|
+
format_version: "1.2",
|
|
38
|
+
resource_changes: [create("aws_eip.pool[0]", 0), create("aws_eip.pool[1]", 1), create("aws_eip.pool[2]", 2), create("aws_s3_bucket.logs")],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("instance nodes for a count block", () => {
|
|
42
|
+
test("a literal count gives the block three instance keys", () => {
|
|
43
|
+
expect(literalInstanceKeys({ count: 3 })).toEqual(["[0]", "[1]", "[2]"]);
|
|
44
|
+
expect(literalInstanceKeys({ count: "${var.n}" })).toBeUndefined();
|
|
45
|
+
expect(literalInstanceKeys({ for_each: { b: 1, a: 2 } })).toEqual(['["a"]', '["b"]']);
|
|
46
|
+
expect(literalInstanceKeys({ for_each: '${toset(["x", "y"])}' })).toEqual(['["x"]', '["y"]']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("the graph has the block node and one instance node per index, each naming the block", async () => {
|
|
50
|
+
const entities = await parseTerraformRootContent(SOURCE, "estate");
|
|
51
|
+
const ir = withInstanceNodes(buildGraphIr(entities), entities);
|
|
52
|
+
const pool = ir.nodes.filter((n) => n.id.startsWith("estate/aws_eip.pool"));
|
|
53
|
+
expect(pool.map((n) => [n.id, n.instanceOf ?? null, n.instanceKey ?? null])).toEqual([
|
|
54
|
+
["estate/aws_eip.pool", null, null],
|
|
55
|
+
["estate/aws_eip.pool[0]", "estate/aws_eip.pool", "[0]"],
|
|
56
|
+
["estate/aws_eip.pool[1]", "estate/aws_eip.pool", "[1]"],
|
|
57
|
+
["estate/aws_eip.pool[2]", "estate/aws_eip.pool", "[2]"],
|
|
58
|
+
]);
|
|
59
|
+
expect(ir.groups.byStack?.estate).toEqual(expect.arrayContaining(["estate/aws_eip.pool[0]", "estate/aws_eip.pool[2]"]));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the stored change set carries three node ids for the three instances", async () => {
|
|
63
|
+
const entities = await parseTerraformRootContent(SOURCE, "estate");
|
|
64
|
+
const ir = withInstanceNodes(buildGraphIr(entities), entities);
|
|
65
|
+
const doc = composeChangeSet([terraformChangeSetPart({ member: "estate", plan: PLAN })]);
|
|
66
|
+
const mapped = mapChangeSetToGraph(doc, { member: "infra", at: null, ids: ir.nodes.map((n) => n.id) });
|
|
67
|
+
|
|
68
|
+
const pool = mapped.entries.filter((e) => e.type === "aws_eip");
|
|
69
|
+
expect(pool.map((e) => [e.address, e.node, e.instanceOf])).toEqual([
|
|
70
|
+
["aws_eip.pool[0]", "infra/estate/aws_eip.pool[0]", "infra/estate/aws_eip.pool"],
|
|
71
|
+
["aws_eip.pool[1]", "infra/estate/aws_eip.pool[1]", "infra/estate/aws_eip.pool"],
|
|
72
|
+
["aws_eip.pool[2]", "infra/estate/aws_eip.pool[2]", "infra/estate/aws_eip.pool"],
|
|
73
|
+
]);
|
|
74
|
+
expect(new Set(pool.map((e) => e.node)).size).toBe(3);
|
|
75
|
+
const logs = mapped.entries.find((e) => e.address === "aws_s3_bucket.logs")!;
|
|
76
|
+
expect(logs.node).toBe("infra/estate/aws_s3_bucket.logs");
|
|
77
|
+
expect(logs.instanceOf).toBeUndefined();
|
|
78
|
+
// Node ids are not hashed.
|
|
79
|
+
expect(mapped.digest).toBe(doc.digest);
|
|
80
|
+
expect(verifyChangeSetDigest(mapped)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("an instance the graph cannot count still gets its instance id, and a drift row the same", async () => {
|
|
84
|
+
const entities = await parseTerraformRootContent(SOURCE.replace("count = 3", "count = var.n"), "estate");
|
|
85
|
+
const ir = withInstanceNodes(buildGraphIr(entities), entities);
|
|
86
|
+
expect(ir.nodes.some((n) => n.instanceOf)).toBe(false);
|
|
87
|
+
const graph = { member: null, at: null, ids: ir.nodes.map((n) => n.id) };
|
|
88
|
+
expect(graphNodeFor({ member: "estate", planner: "terraform", address: "aws_eip.pool[1]" }, graph)).toMatchObject({
|
|
89
|
+
node: "estate/aws_eip.pool[1]",
|
|
90
|
+
instanceOf: "estate/aws_eip.pool",
|
|
91
|
+
});
|
|
92
|
+
// Under an expanded module call the module key sits on the call's segment (#4486).
|
|
93
|
+
expect(graphNodeFor({ member: "estate", planner: "terraform", address: "module.app[0].aws_eip.pool[1]" }, { ...graph, ids: ["estate/module.app/aws_eip.pool"] })).toEqual({
|
|
94
|
+
node: "estate/module.app[0]/aws_eip.pool[1]",
|
|
95
|
+
instanceOf: "estate/module.app/aws_eip.pool",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/changed-roots.ts
CHANGED
|
@@ -9,16 +9,19 @@
|
|
|
9
9
|
* in a local module it calls (followed through modules that call modules),
|
|
10
10
|
* or is one of its var files.
|
|
11
11
|
*
|
|
12
|
-
* The module calls are read with a regex over the root's
|
|
12
|
+
* The module calls are read with a regex over the root's HCL configuration
|
|
13
13
|
* files, the way `./op/activities/live-detect.ts` reads a live block, so
|
|
14
|
-
* this stays out of the HCL parser's dependency graph.
|
|
14
|
+
* this stays out of the HCL parser's dependency graph. Which files those are
|
|
15
|
+
* follows the root's binary through the lexicon's one lister (chant#4457):
|
|
16
|
+
* `.tf`, plus `.tofu` for a tofu or choudoufu root. Only a `source` that
|
|
15
17
|
* starts with `./` or `../` is followed; a registry or git source is a
|
|
16
18
|
* version pin in the root's own files, which a change to it already touches.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
import { existsSync,
|
|
21
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
20
22
|
import { join, posix, relative, resolve, sep } from "node:path";
|
|
21
23
|
import type { TerraformRootConfig } from "./config";
|
|
24
|
+
import { isJsonConfigFile, listConfigFilesSafe } from "./hcl/files";
|
|
22
25
|
|
|
23
26
|
/** `source = "./..."` or `"../..."` inside a module block. Other sources are not on disk here. */
|
|
24
27
|
const LOCAL_SOURCE = /\bsource\s*=\s*"(\.{1,2}\/[^"]*)"/g;
|
|
@@ -31,17 +34,11 @@ function projectRelative(projectRoot: string, dir: string): string {
|
|
|
31
34
|
return rel === "" ? "." : posix.normalize(rel).replace(/\/+$/, "");
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
/** Local module directories `dir` calls, read from
|
|
35
|
-
function localModuleDirs(dir: string): string[] {
|
|
36
|
-
let names: string[];
|
|
37
|
-
try {
|
|
38
|
-
names = readdirSync(dir);
|
|
39
|
-
} catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
37
|
+
/** Local module directories `dir` calls, read from the HCL configuration files `binary` reads. */
|
|
38
|
+
function localModuleDirs(dir: string, binary?: string): string[] {
|
|
42
39
|
const out: string[] = [];
|
|
43
|
-
for (const name of
|
|
44
|
-
if (
|
|
40
|
+
for (const name of listConfigFilesSafe(dir, binary)) {
|
|
41
|
+
if (isJsonConfigFile(name)) continue;
|
|
45
42
|
let text: string;
|
|
46
43
|
try {
|
|
47
44
|
text = readFileSync(join(dir, name), "utf8");
|
|
@@ -61,7 +58,7 @@ function localModuleDirs(dir: string): string[] {
|
|
|
61
58
|
* Every path, relative to `projectRoot`, whose change touches `root`: its
|
|
62
59
|
* directory, each local module it reaches, and its var files.
|
|
63
60
|
*/
|
|
64
|
-
export function rootWatchPaths(projectRoot: string, root: Pick<TerraformRootConfig, "dir" | "varFiles"
|
|
61
|
+
export function rootWatchPaths(projectRoot: string, root: Pick<TerraformRootConfig, "dir" | "varFiles">, binary?: string): string[] {
|
|
65
62
|
const rootDir = resolve(projectRoot, root.dir);
|
|
66
63
|
const seen = new Set<string>();
|
|
67
64
|
const queue = [rootDir];
|
|
@@ -69,7 +66,7 @@ export function rootWatchPaths(projectRoot: string, root: Pick<TerraformRootConf
|
|
|
69
66
|
const dir = queue.shift()!;
|
|
70
67
|
if (seen.has(dir)) continue;
|
|
71
68
|
seen.add(dir);
|
|
72
|
-
queue.push(...localModuleDirs(dir));
|
|
69
|
+
queue.push(...localModuleDirs(dir, binary));
|
|
73
70
|
}
|
|
74
71
|
const paths = [...seen].map((d) => projectRelative(projectRoot, d));
|
|
75
72
|
for (const file of root.varFiles ?? []) paths.push(projectRelative(projectRoot, resolve(rootDir, file)));
|
|
@@ -91,11 +88,12 @@ export function changedRoots(
|
|
|
91
88
|
projectRoot: string,
|
|
92
89
|
roots: Record<string, Pick<TerraformRootConfig, "dir" | "varFiles">>,
|
|
93
90
|
changedFiles: readonly string[],
|
|
91
|
+
binary?: string,
|
|
94
92
|
): string[] {
|
|
95
93
|
const files = changedFiles.map((f) => posix.normalize(f));
|
|
96
94
|
const touched: string[] = [];
|
|
97
95
|
for (const [name, root] of Object.entries(roots)) {
|
|
98
|
-
const watch = rootWatchPaths(projectRoot, root);
|
|
96
|
+
const watch = rootWatchPaths(projectRoot, root, binary);
|
|
99
97
|
if (files.some((f) => watch.some((p) => covers(p, f)))) touched.push(name);
|
|
100
98
|
}
|
|
101
99
|
return touched.sort();
|
package/src/commands.ts
CHANGED
|
@@ -22,9 +22,9 @@ import type { PinRoot } from "./pin";
|
|
|
22
22
|
const USAGE =
|
|
23
23
|
"usage: chant terraform pin-rollout --module <source> --from <pin> --to <pin> [--root <dir>]... [--depends-on <dir>=<dir>]... " +
|
|
24
24
|
"[--ts-source <dir>=<file>]... [--canary <dir>]... [--waves-from <file> [--waves-prefix <dir>]] [--pull-request] " +
|
|
25
|
-
"[--base <branch>] [--remote <name>] [--applied-check <name>] [--json]";
|
|
25
|
+
"[--base <branch>] [--remote <name>] [--applied-check <name>] [--binary <terraform|tofu|choudoufu>] [--json]";
|
|
26
26
|
|
|
27
|
-
const VALUE_FLAGS = new Set(["--module", "--from", "--to", "--root", "--depends-on", "--ts-source", "--canary", "--waves-from", "--waves-prefix", "--base", "--remote", "--applied-check"]);
|
|
27
|
+
const VALUE_FLAGS = new Set(["--module", "--from", "--to", "--root", "--depends-on", "--ts-source", "--canary", "--waves-from", "--waves-prefix", "--base", "--remote", "--applied-check", "--binary"]);
|
|
28
28
|
const BOOLEAN_FLAGS = new Set(["--pull-request", "--json"]);
|
|
29
29
|
|
|
30
30
|
/** Parse the verb's flags into the activity's args. Throws on an unknown flag or a missing value. */
|
|
@@ -75,6 +75,7 @@ export function parsePinRolloutArgs(rawArgs: string[]): { args: TerraformPinRoll
|
|
|
75
75
|
...(one("--base") ? { base: one("--base")! } : {}),
|
|
76
76
|
...(one("--remote") ? { remote: one("--remote")! } : {}),
|
|
77
77
|
...(one("--applied-check") ? { appliedCheck: one("--applied-check")! } : {}),
|
|
78
|
+
...(one("--binary") ? { binary: one("--binary")! } : {}),
|
|
78
79
|
};
|
|
79
80
|
return { args, json: flags.has("--json") };
|
|
80
81
|
}
|
|
@@ -105,8 +106,87 @@ async function runPinRolloutVerb(ctx: CommandGroupContext): Promise<number> {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
const REPORT_USAGE =
|
|
110
|
+
"usage: chant terraform report --stage tf-plan|tf-apply --plan <show.json> [--applied <show-after-apply.json>] (--out <dir> | --to <url>) " +
|
|
111
|
+
"[--project <host/path>] [--commit <sha>] [--pull-request <n>] [--binary <name>] [--binary-version <v>] [--root-dir <dir>] [--self-root <dir>] [--repo <dir>] " +
|
|
112
|
+
"[--started <iso>] [--finished <iso>] [--json]";
|
|
113
|
+
|
|
114
|
+
const REPORT_VALUE_FLAGS = new Set(["--stage", "--plan", "--applied", "--out", "--to", "--project", "--commit", "--pull-request", "--binary", "--binary-version", "--root-dir", "--self-root", "--repo", "--started", "--finished"]);
|
|
115
|
+
|
|
116
|
+
/** Parse `chant terraform report`'s flags. Throws on an unknown flag, a missing value or a missing required flag. */
|
|
117
|
+
export function parseReportArgs(rawArgs: string[]): { values: Map<string, string>; json: boolean } {
|
|
118
|
+
const tokens = splitJoinedFlags(rawArgs, new Set(["--json"]));
|
|
119
|
+
const values = new Map<string, string>();
|
|
120
|
+
let json = false;
|
|
121
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
122
|
+
const t = tokens[i]!;
|
|
123
|
+
if (t === "--json") {
|
|
124
|
+
json = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!REPORT_VALUE_FLAGS.has(t)) throw t.startsWith("-") ? unknownFlagError(t, REPORT_USAGE) : new Error(`unexpected argument "${t}"\n${REPORT_USAGE}`);
|
|
128
|
+
const v = tokens[++i];
|
|
129
|
+
if (v === undefined || v.startsWith("--")) throw new Error(`${t} needs a value\n${REPORT_USAGE}`);
|
|
130
|
+
values.set(t, v);
|
|
131
|
+
}
|
|
132
|
+
const stage = values.get("--stage");
|
|
133
|
+
if (stage !== "tf-plan" && stage !== "tf-apply") throw new Error(`--stage is tf-plan or tf-apply\n${REPORT_USAGE}`);
|
|
134
|
+
if (!values.has("--plan")) throw new Error(`--plan is required\n${REPORT_USAGE}`);
|
|
135
|
+
if (stage === "tf-apply" && !values.has("--applied")) throw new Error(`--stage tf-apply needs --applied, the show -json of a plan made after the apply\n${REPORT_USAGE}`);
|
|
136
|
+
if (values.has("--out") === values.has("--to")) throw new Error(`give one of --out <dir> and --to <url>\n${REPORT_USAGE}`);
|
|
137
|
+
return { values, json };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runReportVerb(ctx: CommandGroupContext): Promise<number> {
|
|
141
|
+
try {
|
|
142
|
+
const { values, json } = parseReportArgs(ctx.rawArgs);
|
|
143
|
+
const { readFileSync } = await import("node:fs");
|
|
144
|
+
const { runFactsFromEnv, terraformReport, writeReport, httpStore, dirStore, moduleEstates, atmosStackReads, topModule } = await import("./report");
|
|
145
|
+
const read = (flag: string): unknown => {
|
|
146
|
+
const file = values.get(flag)!;
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`${flag} ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const run = runFactsFromEnv(process.env, { pullRequest: values.get("--pull-request"), project: values.get("--project"), commit: values.get("--commit") });
|
|
154
|
+
const plan = read("--plan");
|
|
155
|
+
// The plan holds no `live` block: each module's estate is read from its directory, <repo>/<root-dir>/<module>.
|
|
156
|
+
const repo = values.get("--repo") ?? process.env.GITHUB_WORKSPACE ?? process.cwd();
|
|
157
|
+
const modules = [...new Set(((plan as { resource_changes?: { address?: string; module_address?: string }[] }).resource_changes ?? []).map((r) => topModule(String(r.module_address ?? r.address ?? ""))).filter((m): m is string => m !== undefined))];
|
|
158
|
+
const estates = moduleEstates(repo, values.get("--root-dir") ?? "", modules);
|
|
159
|
+
// An Atmos repo wires its components with !terraform.output in the stack manifests, which the plan does not keep.
|
|
160
|
+
const componentReads = atmosStackReads(repo);
|
|
161
|
+
const result = terraformReport({
|
|
162
|
+
stage: values.get("--stage") as "tf-plan" | "tf-apply",
|
|
163
|
+
plan,
|
|
164
|
+
...(Object.keys(estates).length > 0 ? { estates } : {}),
|
|
165
|
+
...(Object.keys(componentReads).length > 0 ? { componentReads } : {}),
|
|
166
|
+
...(values.has("--applied") ? { applied: read("--applied") } : {}),
|
|
167
|
+
run,
|
|
168
|
+
...(values.has("--binary") ? { binary: values.get("--binary")! } : {}),
|
|
169
|
+
...(values.has("--binary-version") ? { binaryVersion: values.get("--binary-version")! } : {}),
|
|
170
|
+
...(values.has("--root-dir") ? { rootDir: values.get("--root-dir")! } : {}),
|
|
171
|
+
...(values.has("--self-root") ? { selfRoot: values.get("--self-root")! } : {}),
|
|
172
|
+
...(values.has("--started") ? { started: values.get("--started")! } : {}),
|
|
173
|
+
finished: values.get("--finished") ?? new Date().toISOString().replace(/\.\d+Z$/, "Z"),
|
|
174
|
+
});
|
|
175
|
+
const store = values.has("--out") ? dirStore(values.get("--out")!) : httpStore(values.get("--to")!);
|
|
176
|
+
const keys = await writeReport(store, result);
|
|
177
|
+
console.log(json ? JSON.stringify({ path: result.path, keys, row: result.row }, null, 2) : `wrote ${keys.join(", ")}`);
|
|
178
|
+
return 0;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
108
185
|
export const terraformCommands: CommandGroup = {
|
|
109
186
|
name: "terraform",
|
|
110
|
-
description: "Terraform and OpenTofu roots: roll a module version out as one pin-bump PR per wave",
|
|
111
|
-
commands: [
|
|
187
|
+
description: "Terraform and OpenTofu roots: roll a module version out as one pin-bump PR per wave; write a run report from a saved plan",
|
|
188
|
+
commands: [
|
|
189
|
+
{ name: "pin-rollout", description: "Read a pin-bump rollout's state and open the next wave's PR when it is due", handler: runPinRolloutVerb },
|
|
190
|
+
{ name: "report", description: "Write a terragucci.report/v1 run report, its change set and its index row from show -json of a composed plan", handler: runReportVerb },
|
|
191
|
+
],
|
|
112
192
|
};
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* and run applies the plan a wave approved rather than planning again.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
7
10
|
|
|
8
11
|
const calls: string[] = [];
|
|
9
12
|
let applyResult: Record<string, unknown> = { applied: true };
|
|
@@ -15,6 +18,10 @@ let waveApply: Record<string, unknown> = { exitCode: 0, landed: true, message: "
|
|
|
15
18
|
const waveApplyArgs: Array<Record<string, unknown>> = [];
|
|
16
19
|
// #4300: the targets each plan was asked for.
|
|
17
20
|
const planTargets: Array<string[] | undefined> = [];
|
|
21
|
+
// #4479: the `text` each plan was asked for.
|
|
22
|
+
const planTexts: Array<boolean | undefined> = [];
|
|
23
|
+
// #4463: a plan that fails, as one with a stand-in a validation block rejects does.
|
|
24
|
+
let planError: Error | undefined;
|
|
18
25
|
|
|
19
26
|
vi.mock("../op/activities/terraform", () => ({
|
|
20
27
|
choudoufuWaveRoot: vi.fn(async () => waveRoot),
|
|
@@ -25,7 +32,7 @@ vi.mock("../op/activities/terraform", () => ({
|
|
|
25
32
|
rootDigest: `sha256:root-${args.root}`,
|
|
26
33
|
planDigest: `jcs1-sha256:${args.root}`,
|
|
27
34
|
changed: true,
|
|
28
|
-
json:
|
|
35
|
+
json: planJson,
|
|
29
36
|
changeSet: { member: { member: args.root, nativeDigest: `sha256:root-${args.root}` }, entries: [] },
|
|
30
37
|
planSetFile: `/x/.chant/choudoufu-waves/${args.root}/plan-set.json`,
|
|
31
38
|
root: `estates/${args.root}`,
|
|
@@ -40,9 +47,11 @@ vi.mock("../op/activities/terraform", () => ({
|
|
|
40
47
|
calls.push(`init ${args.root}`);
|
|
41
48
|
return { dir: `/x/${args.root}` };
|
|
42
49
|
}),
|
|
43
|
-
terraformPlan: vi.fn(async (args: { root: string; vars?: Record<string, unknown>; targets?: string[] }) => {
|
|
50
|
+
terraformPlan: vi.fn(async (args: { root: string; vars?: Record<string, unknown>; targets?: string[]; text?: boolean }) => {
|
|
44
51
|
calls.push(`plan ${args.root} ${JSON.stringify(args.vars ?? {})}`);
|
|
45
52
|
planTargets.push(args.targets);
|
|
53
|
+
planTexts.push(args.text);
|
|
54
|
+
if (planError) throw planError;
|
|
46
55
|
return { planFile: "chant.tfplan", planDigest: `jcs1-sha256:${args.root}`, json: planJson, ...(changeSet ? { changeSet } : {}) };
|
|
47
56
|
}),
|
|
48
57
|
terraformApply: vi.fn(async (args: { root: string; planFile: string }) => {
|
|
@@ -67,6 +76,19 @@ beforeEach(() => {
|
|
|
67
76
|
waveApply = { exitCode: 0, landed: true, message: "landed" };
|
|
68
77
|
waveApplyArgs.length = 0;
|
|
69
78
|
planTargets.length = 0;
|
|
79
|
+
planTexts.length = 0;
|
|
80
|
+
planError = undefined;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("terraform-apply plans with one show -json (#4479)", () => {
|
|
84
|
+
test("plan and run ask terraformPlan to skip the -no-color render, and plan takes outputs and shape off the one json", async () => {
|
|
85
|
+
planJson = { planned_values: { outputs: { id: { value: "x" } } }, resource_changes: [] };
|
|
86
|
+
const planned = await terraformApplyCapability.plan!({ env: "test", component: "e01" }, { root: "e01" });
|
|
87
|
+
await terraformApplyCapability.run({ env: "test", component: "e01" }, { root: "e01" });
|
|
88
|
+
expect(planTexts).toEqual([false, false]);
|
|
89
|
+
expect(planned.outputs).toEqual({ known: { id: "x" }, unknown: [] });
|
|
90
|
+
expect(planned.shape).toBeDefined();
|
|
91
|
+
});
|
|
70
92
|
});
|
|
71
93
|
|
|
72
94
|
describe("terraform-apply targets (#4300)", () => {
|
|
@@ -191,10 +213,35 @@ describe("terraform-apply", () => {
|
|
|
191
213
|
planDigest: "jcs1-sha256:app",
|
|
192
214
|
artifact: { planFile: "chant.tfplan", planDigest: "jcs1-sha256:app" },
|
|
193
215
|
outputs: { known: {}, unknown: [] },
|
|
216
|
+
shape: { member: "app", entries: [], unknownInputs: [] },
|
|
194
217
|
});
|
|
195
218
|
expect(calls).toEqual(["init app", 'plan app {"vpc":"v-1"}']);
|
|
196
219
|
});
|
|
197
220
|
|
|
221
|
+
test("plan returns the shape of the plan's show -json, on the plain path and the choudoufu set path (#4436)", async () => {
|
|
222
|
+
planJson = {
|
|
223
|
+
resource_changes: [
|
|
224
|
+
{ address: "terraform_data.keep", type: "terraform_data", change: { actions: ["no-op"] } },
|
|
225
|
+
{ address: "terraform_data.web", type: "terraform_data", change: { actions: ["create"], before: null, after: { input: "x" }, after_unknown: { id: true } } },
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
const expected = {
|
|
229
|
+
member: "app",
|
|
230
|
+
entries: [{ logicalId: "terraform_data.web", action: "create", type: "terraform_data", properties: ["id", "input"], unknown: ["id"] }],
|
|
231
|
+
unknownInputs: [],
|
|
232
|
+
};
|
|
233
|
+
const plain = await terraformApplyCapability.plan!({ env: "test", component: "app" }, { root: "app" });
|
|
234
|
+
expect(plain.shape).toEqual(expected);
|
|
235
|
+
waveRoot = { wave: true, root: "estates/app" };
|
|
236
|
+
const set = await terraformApplyCapability.plan!({ env: "test", component: "app" }, { root: "app" });
|
|
237
|
+
expect(set.shape).toEqual(expected);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a root waiting on unknown outputs that cannot be previewed returns no shape, so it stays uncovered (#4436)", async () => {
|
|
241
|
+
const planned = await terraformApplyCapability.plan!({ env: "test", component: "app", unresolvedInputs: ["net"] }, { root: "app", vars: { vpc: undefined } });
|
|
242
|
+
expect(planned.shape).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
198
245
|
test("plan answers the outputs the root will have, from the plan's planned values (#3999)", async () => {
|
|
199
246
|
planJson = {
|
|
200
247
|
planned_values: { outputs: { cidr: { sensitive: false, value: "10.1.0.0/16" }, id: { sensitive: false } } },
|
|
@@ -209,14 +256,21 @@ describe("terraform-apply", () => {
|
|
|
209
256
|
expect(planned.artifact).toEqual({ planFile: "chant.tfplan", planDigest: "jcs1-sha256:net" });
|
|
210
257
|
});
|
|
211
258
|
|
|
212
|
-
test("a root whose vars read outputs not known yet is not planned: a hole names whose (#3999)", async () => {
|
|
259
|
+
test("a root whose vars read outputs not known yet and that cannot be previewed is not planned: a hole names whose (#3999, #4463)", async () => {
|
|
260
|
+
// No chant.config.* above the working directory, so the root's variables cannot be read.
|
|
213
261
|
const ctx = { env: "test", component: "app", unresolvedInputs: ["net"] };
|
|
214
262
|
const planned = await terraformApplyCapability.plan!(ctx, { root: "app", vars: { vpc: undefined, name: "app" } });
|
|
215
263
|
expect(calls).toEqual([]);
|
|
216
264
|
expect(planned.artifact).toBeUndefined();
|
|
217
265
|
expect(planned.outputs).toBeUndefined();
|
|
218
266
|
expect(planned.changeSet?.entries).toEqual([]);
|
|
219
|
-
expect(planned.changeSet?.member.holes).toEqual([
|
|
267
|
+
expect(planned.changeSet?.member.holes).toEqual([
|
|
268
|
+
{
|
|
269
|
+
address: "app",
|
|
270
|
+
type: "terraform_root",
|
|
271
|
+
reason: 'inputs from net; planned again once they apply; no preview: no root named "app" in terraform.roots to read its variables from',
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
220
274
|
expect(planned.planDigest).toBe(planned.changeSet?.member.planDigest);
|
|
221
275
|
const again = await terraformApplyCapability.plan!(ctx, { root: "app", vars: { vpc: undefined, name: "app" } });
|
|
222
276
|
expect(again.planDigest).toBe(planned.planDigest);
|
|
@@ -306,3 +360,116 @@ describe("terraform-apply, continued", () => {
|
|
|
306
360
|
expect(isPlanMovedError(err)).toBe(false);
|
|
307
361
|
});
|
|
308
362
|
});
|
|
363
|
+
|
|
364
|
+
describe("terraform-apply previews a root outside a group with placeholders (#4463)", () => {
|
|
365
|
+
let project: string;
|
|
366
|
+
|
|
367
|
+
beforeEach(() => {
|
|
368
|
+
project = mkdtempSync(join(tmpdir(), "chant-tf-preview-"));
|
|
369
|
+
mkdirSync(join(project, "app"));
|
|
370
|
+
writeFileSync(join(project, "chant.config.json"), JSON.stringify({ lexicons: ["terraform"], terraform: { roots: { app: { dir: "app" } } } }));
|
|
371
|
+
writeFileSync(
|
|
372
|
+
join(project, "app", "variables.tf"),
|
|
373
|
+
'variable "vpc_id" {\n type = string\n}\n\nvariable "name" {\n type = string\n}\n',
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
afterEach(() => rmSync(project, { recursive: true, force: true }));
|
|
378
|
+
|
|
379
|
+
const subnetPlan = (vpcAfter: unknown) => ({
|
|
380
|
+
configuration: {
|
|
381
|
+
root_module: {
|
|
382
|
+
resources: [
|
|
383
|
+
{
|
|
384
|
+
address: "aws_subnet.a",
|
|
385
|
+
mode: "managed",
|
|
386
|
+
type: "aws_subnet",
|
|
387
|
+
name: "a",
|
|
388
|
+
expressions: { vpc_id: { references: ["var.vpc_id"] }, tags: { references: ["var.name"] } },
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
outputs: { subnet_vpc: { expression: { references: ["var.vpc_id"] } } },
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
resource_changes: [
|
|
395
|
+
{
|
|
396
|
+
address: "aws_subnet.a",
|
|
397
|
+
mode: "managed",
|
|
398
|
+
type: "aws_subnet",
|
|
399
|
+
name: "a",
|
|
400
|
+
change: { actions: ["create"], before: null, after: { vpc_id: vpcAfter, tags: { Name: "app" } }, after_unknown: { id: true } },
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
planned_values: { outputs: { subnet_vpc: { value: vpcAfter } } },
|
|
404
|
+
output_changes: { subnet_vpc: { actions: ["create"], after: vpcAfter, after_unknown: false } },
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const ctx = () => ({ env: "test", component: "app", unresolvedInputs: ["net"] });
|
|
408
|
+
const input = () => ({ root: "app", cwd: project, vars: { vpc_id: undefined, name: "app" } });
|
|
409
|
+
|
|
410
|
+
test("plans with a typed stand-in, marks what it drives unknown, and says which reads were placeholders", async () => {
|
|
411
|
+
planJson = subnetPlan("known-after-apply");
|
|
412
|
+
const planned = await terraformApplyCapability.plan!(ctx(), input());
|
|
413
|
+
expect(calls).toEqual(["init app", 'plan app {"vpc_id":"known-after-apply","name":"app"}']);
|
|
414
|
+
expect(planned.changeSet?.member.holes).toEqual([]);
|
|
415
|
+
expect(planned.changeSet?.member.placeholders).toEqual(["var.vpc_id"]);
|
|
416
|
+
expect(planned.changeSet?.member.planDigest).toBe(planned.planDigest);
|
|
417
|
+
const attrs = planned.changeSet?.entries[0]?.attributes ?? [];
|
|
418
|
+
expect(attrs.find((a) => a.path === "vpc_id")).toEqual({ path: "vpc_id", unknown: true });
|
|
419
|
+
// No stand-in value reaches the note.
|
|
420
|
+
expect(JSON.stringify(planned.changeSet)).not.toContain("known-after-apply");
|
|
421
|
+
expect(planned.outputs).toEqual({ known: {}, unknown: ["subnet_vpc"] });
|
|
422
|
+
expect(planned.shape?.unknownInputs).toEqual(["net"]);
|
|
423
|
+
expect(planned.shape?.entries[0]).toMatchObject({ logicalId: "aws_subnet.a", action: "create", unknown: ["id", "vpc_id"] });
|
|
424
|
+
// The artifact is the shape, never a plan file a stand-in could apply from.
|
|
425
|
+
expect(planned.artifact).toMatchObject({ kind: "terraform-apply-shape", member: "app" });
|
|
426
|
+
expect((planned.artifact as { planFile?: string }).planFile).toBeUndefined();
|
|
427
|
+
const again = await terraformApplyCapability.plan!(ctx(), input());
|
|
428
|
+
expect(again.planDigest).toBe(planned.planDigest);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a placeholder plan that fails leaves the member a hole, and the hole says why (ruling 2026-10-25)", async () => {
|
|
432
|
+
planError = new Error("terraform plan failed in /x/app (exit 1)\nError: Invalid value for variable\n on variables.tf line 1: vpc_id must start with vpc-");
|
|
433
|
+
const planned = await terraformApplyCapability.plan!(ctx(), input());
|
|
434
|
+
expect(planned.artifact).toBeUndefined();
|
|
435
|
+
expect(planned.shape).toBeUndefined();
|
|
436
|
+
expect(planned.changeSet?.entries).toEqual([]);
|
|
437
|
+
expect(planned.changeSet?.member.placeholders).toBeUndefined();
|
|
438
|
+
const [hole] = planned.changeSet?.member.holes ?? [];
|
|
439
|
+
expect(hole?.reason).toMatch(/^inputs from net; planned again once they apply; no preview: the plan with stand-in values failed: /);
|
|
440
|
+
expect(hole?.reason).toContain("Invalid value for variable");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("a count over a stand-in leaves the member a hole, and the hole says why", async () => {
|
|
444
|
+
const plan = subnetPlan("known-after-apply") as Record<string, any>;
|
|
445
|
+
plan.configuration.root_module.resources[0].count_expression = { references: ["var.vpc_id"] };
|
|
446
|
+
planJson = plan;
|
|
447
|
+
const planned = await terraformApplyCapability.plan!(ctx(), input());
|
|
448
|
+
expect(planned.changeSet?.member.holes[0]?.reason).toContain("no preview: the count or for_each of aws_subnet.a reads a stand-in");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("run holds the real plan to the preview's shape: a match applies it", async () => {
|
|
452
|
+
planJson = subnetPlan("known-after-apply");
|
|
453
|
+
const planned = await terraformApplyCapability.plan!(ctx(), input());
|
|
454
|
+
calls.length = 0;
|
|
455
|
+
planJson = subnetPlan("vpc-123");
|
|
456
|
+
const out = await terraformApplyCapability.run({ env: "test", component: "app", plans: { app: planned.artifact } }, { root: "app", cwd: project, vars: { vpc_id: "vpc-123", name: "app" } });
|
|
457
|
+
expect(calls).toEqual(["init app", 'plan app {"vpc_id":"vpc-123","name":"app"}', "apply app chant.tfplan", "output app"]);
|
|
458
|
+
expect(out.fromWavePlan).toBe(false);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("run refuses a real plan that goes beyond the preview's shape, and applies nothing", async () => {
|
|
462
|
+
planJson = subnetPlan("known-after-apply");
|
|
463
|
+
const planned = await terraformApplyCapability.plan!(ctx(), input());
|
|
464
|
+
calls.length = 0;
|
|
465
|
+
const real = subnetPlan("vpc-123") as Record<string, any>;
|
|
466
|
+
real.resource_changes.push({ address: "aws_route.r", mode: "managed", type: "aws_route", name: "r", change: { actions: ["create"], before: null, after: { cidr: "0.0.0.0/0" }, after_unknown: {} } });
|
|
467
|
+
planJson = real;
|
|
468
|
+
const err = await terraformApplyCapability
|
|
469
|
+
.run({ env: "test", component: "app", plans: { app: planned.artifact } }, { root: "app", cwd: project, vars: { vpc_id: "vpc-123", name: "app" } })
|
|
470
|
+
.catch((e: unknown) => e);
|
|
471
|
+
expect(isPlanMovedError(err)).toBe(true);
|
|
472
|
+
expect((err as Error).message).toContain("aws_route.r");
|
|
473
|
+
expect(calls.some((c) => c.startsWith("apply"))).toBe(false);
|
|
474
|
+
});
|
|
475
|
+
});
|