@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
|
@@ -11,8 +11,17 @@
|
|
|
11
11
|
* The roots use `terraform_data`, which is built into `tofu` and `terraform`,
|
|
12
12
|
* with local state, so nothing downloads a provider or reaches a cloud. The
|
|
13
13
|
* estate is net -> a, b and a -> app, with dns on its own; a and b read net's
|
|
14
|
-
* output and app reads a's, through `stackOutput()`.
|
|
15
|
-
*
|
|
14
|
+
* output and app reads a's, through `stackOutput()`. It lives in
|
|
15
|
+
* `../__fixtures__/pr-loop`, whose README also gives chant#4463's recorded
|
|
16
|
+
* run over it. Skipped when neither binary is on the PATH.
|
|
17
|
+
*
|
|
18
|
+
* app is previewed with a placeholder for `var.subnet` (#4463). A pull
|
|
19
|
+
* request that makes a's id end in `-v2` stays within that preview, so one
|
|
20
|
+
* approval applies both roots. One that makes it end in `-wide` adds
|
|
21
|
+
* `terraform_data.wide[0]` to app's real plan through a count the preview
|
|
22
|
+
* does not follow, so app's round 2 waits on its own gate (#4487).
|
|
23
|
+
*
|
|
24
|
+
* Composed runs over three roots are `pr-loop-compose.tofu.e2e.test.ts`.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
import { execFileSync } from "node:child_process";
|
|
@@ -22,7 +31,7 @@ import { dirname, join } from "node:path";
|
|
|
22
31
|
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
|
23
32
|
import type { ParsedArgs } from "@intentius/chant/cli/registry";
|
|
24
33
|
import type { PrReport } from "@intentius/chant/pr-loop";
|
|
25
|
-
import { buildGraphIr } from "@intentius/chant/graph-ir";
|
|
34
|
+
import { buildGraphIr, withInstanceNodes } from "@intentius/chant/graph-ir";
|
|
26
35
|
import { terraformPlugin } from "../plugin";
|
|
27
36
|
import { renderTerraformRoots } from "../hcl/roots";
|
|
28
37
|
|
|
@@ -38,40 +47,27 @@ function which(bin: string): boolean {
|
|
|
38
47
|
}
|
|
39
48
|
const BINARY = which("tofu") ? "tofu" : which("terraform") ? "terraform" : undefined;
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
app: 'variable "subnet" {\n type = string\n}\n\nresource "terraform_data" "app" {\n input = "app on ${var.subnet}"\n}\n',
|
|
46
|
-
dns: 'resource "terraform_data" "zone" {\n input = "example.test"\n}\n',
|
|
47
|
-
};
|
|
50
|
+
/** The workspace, `../__fixtures__/pr-loop` (its README says what each pull request below exercises). */
|
|
51
|
+
const FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "pr-loop");
|
|
52
|
+
const ROOT_NAMES = ["net", "a", "b", "app", "dns"];
|
|
53
|
+
const ROOTS: Record<string, string> = Object.fromEntries(ROOT_NAMES.map((r) => [r, readFileSync(join(FIXTURE, "roots", r, "main.tf"), "utf8")]));
|
|
48
54
|
|
|
49
55
|
// The source passes `chant lint`, since `chant graph --format ir`, which maps
|
|
50
56
|
// the stored change set onto nodes (#4022), refuses source that does not.
|
|
51
57
|
/**
|
|
52
58
|
* The estate's graph node ids, read in process with the pair `chant graph
|
|
53
59
|
* --format ir` runs for terraform roots (`renderTerraformRoots`, then
|
|
54
|
-
* `buildGraphIr`), so pr-apply maps its stored change set without starting a
|
|
60
|
+
* `buildGraphIr` and `withInstanceNodes`), so pr-apply maps its stored change set without starting a
|
|
55
61
|
* chant child process. `change-set-graph.e2e.test.ts` holds these ids equal
|
|
56
62
|
* to the spawned `chant graph --format ir`'s.
|
|
57
63
|
*/
|
|
58
64
|
async function estateGraphIds(projectRoot: string): Promise<{ ids: string[] }> {
|
|
59
65
|
const config = JSON.parse(readFileSync(join(projectRoot, "chant.config.json"), "utf8")) as { terraform: { binary?: string; roots: Record<string, { dir: string }> } };
|
|
60
66
|
const rendered = await renderTerraformRoots({ projectRoot, roots: config.terraform.roots, ...(config.terraform.binary ? { binary: config.terraform.binary } : {}) });
|
|
61
|
-
return { ids: buildGraphIr(rendered.entities).nodes.map((n) => n.id) };
|
|
67
|
+
return { ids: withInstanceNodes(buildGraphIr(rendered.entities), rendered.entities).nodes.map((n) => n.id) };
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
const COMPONENTS =
|
|
65
|
-
|
|
66
|
-
const apply = (root: string, vars?: Record<string, unknown>) =>
|
|
67
|
-
[phase("Apply", [{ kind: "terraform-apply", root, noRollback: "test roots with local state", vars: vars ?? {} }])];
|
|
68
|
-
|
|
69
|
-
export const net: Component = { name: "net", dependsOn: [], deploy: apply("net") };
|
|
70
|
-
export const a: Component = { name: "a", dependsOn: ["net"], deploy: apply("a", { cidr: stackOutput("net", "cidr") }) };
|
|
71
|
-
export const b: Component = { name: "b", dependsOn: ["net"], deploy: apply("b", { cidr: stackOutput("net", "cidr") }) };
|
|
72
|
-
export const app: Component = { name: "app", dependsOn: ["a"], deploy: apply("app", { subnet: stackOutput("a", "id") }) };
|
|
73
|
-
export const dns: Component = { name: "dns", dependsOn: [], deploy: apply("dns") };
|
|
74
|
-
`;
|
|
70
|
+
const COMPONENTS = readFileSync(join(FIXTURE, "estate.component.ts"), "utf8");
|
|
75
71
|
|
|
76
72
|
/** GitHub as far as the loop uses it: comments, statuses, reviews and the merged pull request of a commit. */
|
|
77
73
|
class FakeGithub {
|
|
@@ -215,14 +211,20 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
215
211
|
expect(plan.status).toBe("planned");
|
|
216
212
|
expect(plan.selection).toMatchObject({ changed: ["a"], dependents: ["app"], waves: [["a"], ["app"]] });
|
|
217
213
|
// a's id is terraform_data's output, which tofu knows only after apply,
|
|
218
|
-
// so app is not planned against the id a has now (#3999)
|
|
219
|
-
//
|
|
214
|
+
// so app is not planned against the id a has now (#3999). It is previewed
|
|
215
|
+
// with a stand-in for var.subnet instead (#4463): its update is in the
|
|
216
|
+
// change set, and the note says the approval covers app's plan once a
|
|
217
|
+
// applies only while that plan does what the preview shows.
|
|
220
218
|
expect(plan.members.map((m) => [m.member, m.counts.update, m.unknownInputs])).toEqual([
|
|
221
219
|
["a", 1, undefined],
|
|
222
|
-
["app",
|
|
220
|
+
["app", 1, ["a.id"]],
|
|
223
221
|
]);
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
const app = plan.changeSet.members.find((m) => m.member === "app");
|
|
223
|
+
expect(app?.holes).toEqual([]);
|
|
224
|
+
expect(app?.placeholders).toEqual(["var.subnet"]);
|
|
225
|
+
expect(plan.changeSet.entries.map((e) => [e.member, e.address, e.action])).toEqual([
|
|
226
|
+
["a", "terraform_data.subnet", "update"],
|
|
227
|
+
["app", "terraform_data.app", "update"],
|
|
226
228
|
]);
|
|
227
229
|
expect(plan.plannedOutputs).toEqual({ a: [] });
|
|
228
230
|
expect(plan.deployedOutputs).toEqual({ net: ["cidr"] });
|
|
@@ -233,6 +235,7 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
233
235
|
expect(note.body.startsWith("<!-- chant-pr:local -->")).toBe(true);
|
|
234
236
|
expect(note.body).toContain(`chant approve pr-1 pr-apply --plan ${plan.digest} --approver github:<your-login> --sign`);
|
|
235
237
|
expect(note.body).toContain("Known only after apply: `app` reads `a.id`.");
|
|
238
|
+
expect(note.body).toContain("previewed with placeholders for `var.subnet`");
|
|
236
239
|
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/plan", state: "success", sha: plan.head });
|
|
237
240
|
}, 240_000);
|
|
238
241
|
|
|
@@ -241,43 +244,29 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
241
244
|
github.reviews[1] = [{ user: { login: "alice" }, state: "APPROVED" }];
|
|
242
245
|
expect((await recordGateApproval("pr-1", "pr-apply", { actor: "github:alice", plan: plan.digest })).ok).toBe(true);
|
|
243
246
|
|
|
244
|
-
// a applies; app
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(
|
|
248
|
-
expect(waiting.status).toBe("waiting");
|
|
249
|
-
expect(waiting.members.map((m) => [m.member, m.status, m.round])).toEqual([
|
|
250
|
-
["a", "applied", undefined],
|
|
251
|
-
["app", "waiting", 2],
|
|
252
|
-
]);
|
|
253
|
-
const round = waiting.rounds![0]!;
|
|
254
|
-
expect(round).toMatchObject({ round: 2, gate: "pr-apply-round-2", components: ["app"], status: "waiting", approval: { status: "pending" } });
|
|
255
|
-
expect(round.changeSet.entries.map((e) => [e.member, e.address, e.action])).toEqual([["app", "terraform_data.app", "update"]]);
|
|
256
|
-
expect(state("a")).toContain("10.0.0.0/16/a-v2");
|
|
257
|
-
expect(state("app")).not.toContain("a-v2");
|
|
258
|
-
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "pending" });
|
|
259
|
-
expect(github.comments.find((c) => c.pr === 1)!.body).toContain(`chant approve pr-1 pr-apply-round-2 --plan ${round.digest}`);
|
|
260
|
-
}, 240_000);
|
|
261
|
-
|
|
262
|
-
test("once the reviewer approves the round's plan, the same apply carries on from its record and applies the dependent (#3999)", async () => {
|
|
263
|
-
const plan = report("plan");
|
|
264
|
-
const round = report("apply").rounds![0]!;
|
|
265
|
-
expect((await recordGateApproval("pr-1", "pr-apply-round-2", { actor: "github:alice", plan: round.digest })).ok).toBe(true);
|
|
266
|
-
expect(await handlers.runComponentsPrApply(ctx({ base: mergedFrom, forge: "github", requireReview: true, resume: RESUME }) as never)).toBe(0);
|
|
247
|
+
// a applies; app is planned again against the id a applied as. That plan
|
|
248
|
+
// does what the placeholder preview showed, with a.id filled in, so the
|
|
249
|
+
// first approval covers it and it applies in the same run (#4463).
|
|
250
|
+
expect(await merge(1)).toBe(0);
|
|
267
251
|
const applied = report("apply");
|
|
268
252
|
expect(applied.digest).toBe(plan.digest);
|
|
269
253
|
expect(applied.status).toBe("applied");
|
|
270
254
|
expect(applied.approval).toEqual({ status: "approved", approvedBy: ["github:alice"] });
|
|
271
|
-
expect(applied.resumed).toEqual({ applied: ["a"] });
|
|
272
255
|
expect(applied.members.map((m) => [m.member, m.status, m.round])).toEqual([
|
|
273
256
|
["a", "applied", undefined],
|
|
274
257
|
["app", "applied", 2],
|
|
275
258
|
]);
|
|
276
|
-
|
|
259
|
+
const round = applied.rounds![0]!;
|
|
260
|
+
expect(round).toMatchObject({ round: 2, gate: "pr-apply-round-2", components: ["app"], status: "applied" });
|
|
261
|
+
expect(round.covered).toEqual({ digest: plan.digest, filledInputs: ["a.id"] });
|
|
262
|
+
expect(round.notCovered).toBeUndefined();
|
|
263
|
+
expect(round.changeSet.entries.map((e) => [e.member, e.address, e.action])).toEqual([["app", "terraform_data.app", "update"]]);
|
|
264
|
+
expect(state("a")).toContain("10.0.0.0/16/a-v2");
|
|
277
265
|
expect(state("app")).toContain("app on 10.0.0.0/16/a-v2");
|
|
278
|
-
// The same note, now with the apply's outcome.
|
|
279
266
|
expect(github.comments.filter((c) => c.pr === 1)).toHaveLength(1);
|
|
280
|
-
|
|
267
|
+
const body = github.comments.find((c) => c.pr === 1)!.body;
|
|
268
|
+
expect(body).toContain("### chant apply for `local`");
|
|
269
|
+
expect(body).toContain(`covered by the approval of \`${plan.digest}\`, with \`a.id\` filled in, so it applied without a new approval`);
|
|
281
270
|
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "success", sha: applied.head });
|
|
282
271
|
}, 240_000);
|
|
283
272
|
|
|
@@ -291,9 +280,10 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
291
280
|
expect(stored.digest).toBe(applied.digest);
|
|
292
281
|
expect(stored.graph).toEqual({ member: null, at: applied.head });
|
|
293
282
|
// The plan's addresses, each on the block the terraform lexicon's graph names `<root>/<address>`.
|
|
294
|
-
// app's
|
|
283
|
+
// app's entry is its placeholder preview (#4463).
|
|
295
284
|
expect(stored.entries.map((e) => [e.member, e.address, e.action, e.node])).toEqual([
|
|
296
285
|
["a", "terraform_data.subnet", "update", "a/terraform_data.subnet"],
|
|
286
|
+
["app", "terraform_data.app", "update", "app/terraform_data.app"],
|
|
297
287
|
]);
|
|
298
288
|
// And those are ids the estate's graph has.
|
|
299
289
|
const graph = await estateGraphIds(dir);
|
|
@@ -325,4 +315,70 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
325
315
|
const gate = gates.find((g) => g.component === "pr-2")!;
|
|
326
316
|
expect(gate).toMatchObject({ state: "pending", planDigest: refused.digest, changeSet: refused.digest });
|
|
327
317
|
}, 240_000);
|
|
318
|
+
|
|
319
|
+
// A round the first approval does not cover, over real tofu (#4487). app's
|
|
320
|
+
// `wide` count reads var.subnet through a local, so the preview's stand-in
|
|
321
|
+
// plans no wide instance; a's real id ends in `-wide`, so app's real plan
|
|
322
|
+
// creates one, which is more than the preview showed.
|
|
323
|
+
test("a dependent whose real plan exceeds its placeholder preview waits on its own round-2 gate (#4487)", async () => {
|
|
324
|
+
const plan = await openPullRequest(3, "a-wide");
|
|
325
|
+
expect(plan.status).toBe("planned");
|
|
326
|
+
expect(plan.members.map((m) => [m.member, m.counts.create, m.counts.update, m.unknownInputs])).toEqual([
|
|
327
|
+
["a", 0, 1, undefined],
|
|
328
|
+
["app", 0, 1, ["a.id"]],
|
|
329
|
+
]);
|
|
330
|
+
expect(plan.changeSet.members.find((m) => m.member === "app")?.placeholders).toEqual(["var.subnet"]);
|
|
331
|
+
expect(plan.changeSet.entries.map((e) => [e.member, e.address, e.action])).toEqual([
|
|
332
|
+
["a", "terraform_data.subnet", "update"],
|
|
333
|
+
["app", "terraform_data.app", "update"],
|
|
334
|
+
]);
|
|
335
|
+
github.reviews[3] = [{ user: { login: "alice" }, state: "APPROVED" }];
|
|
336
|
+
expect((await recordGateApproval("pr-3", "pr-apply", { actor: "github:alice", plan: plan.digest })).ok).toBe(true);
|
|
337
|
+
|
|
338
|
+
// a applies; app, planned now against the id a applied as, goes beyond the preview and waits on its own gate.
|
|
339
|
+
expect(await merge(3)).toBe(3);
|
|
340
|
+
const waiting = report("apply");
|
|
341
|
+
expect(waiting.digest).toBe(plan.digest);
|
|
342
|
+
expect(waiting.status).toBe("waiting");
|
|
343
|
+
expect(waiting.members.map((m) => [m.member, m.status, m.round])).toEqual([
|
|
344
|
+
["a", "applied", undefined],
|
|
345
|
+
["app", "waiting", 2],
|
|
346
|
+
]);
|
|
347
|
+
const round = waiting.rounds![0]!;
|
|
348
|
+
expect(round).toMatchObject({ round: 2, gate: "pr-apply-round-2", components: ["app"], status: "waiting", approval: { status: "pending" } });
|
|
349
|
+
expect(round.changeSet.entries.map((e) => [e.member, e.address, e.action])).toEqual([
|
|
350
|
+
["app", "terraform_data.app", "update"],
|
|
351
|
+
["app", "terraform_data.wide[0]", "create"],
|
|
352
|
+
]);
|
|
353
|
+
// The fresh plan's shape adds a resource the preview did not show, so the first approval does not cover it, and the reason names it.
|
|
354
|
+
expect(round.covered).toBeUndefined();
|
|
355
|
+
expect(round.notCovered).toContain("terraform_data.wide");
|
|
356
|
+
expect(state("a")).toContain("10.0.0.0/16/a-wide");
|
|
357
|
+
expect(state("app")).not.toContain("a-wide");
|
|
358
|
+
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "pending" });
|
|
359
|
+
expect(github.comments.find((c) => c.pr === 3)!.body).toContain(`chant approve pr-3 pr-apply-round-2 --plan ${round.digest}`);
|
|
360
|
+
}, 240_000);
|
|
361
|
+
|
|
362
|
+
test("once the reviewer approves the round's plan, the same apply carries on from its record and applies the dependent (#4487)", async () => {
|
|
363
|
+
const plan = report("plan");
|
|
364
|
+
const round = report("apply").rounds![0]!;
|
|
365
|
+
expect((await recordGateApproval("pr-3", "pr-apply-round-2", { actor: "github:alice", plan: round.digest })).ok).toBe(true);
|
|
366
|
+
expect(await handlers.runComponentsPrApply(ctx({ base: mergedFrom, forge: "github", requireReview: true, resume: RESUME }) as never)).toBe(0);
|
|
367
|
+
const applied = report("apply");
|
|
368
|
+
expect(applied.digest).toBe(plan.digest);
|
|
369
|
+
expect(applied.status).toBe("applied");
|
|
370
|
+
expect(applied.approval).toEqual({ status: "approved", approvedBy: ["github:alice"] });
|
|
371
|
+
expect(applied.resumed).toEqual({ applied: ["a"] });
|
|
372
|
+
expect(applied.members.map((m) => [m.member, m.status, m.round])).toEqual([
|
|
373
|
+
["a", "applied", undefined],
|
|
374
|
+
["app", "applied", 2],
|
|
375
|
+
]);
|
|
376
|
+
expect(applied.rounds?.map((r) => [r.digest, r.status, r.approval.approvedBy])).toEqual([[round.digest, "applied", ["github:alice"]]]);
|
|
377
|
+
expect(state("app")).toContain("app on 10.0.0.0/16/a-wide");
|
|
378
|
+
expect(state("app")).toContain("wide on 10.0.0.0/16/a-wide");
|
|
379
|
+
// The same note, now with the apply's outcome.
|
|
380
|
+
expect(github.comments.filter((c) => c.pr === 3)).toHaveLength(1);
|
|
381
|
+
expect(github.comments.find((c) => c.pr === 3)!.body).toContain("### chant apply for `local`");
|
|
382
|
+
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "success", sha: applied.head });
|
|
383
|
+
}, 240_000);
|
|
328
384
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each member's outcome of a composed apply (#4442), read from recorded
|
|
3
|
+
* `apply -json` output of a run of the three-module plan that failed in its
|
|
4
|
+
* second module (`../__fixtures__/compose/apply-failed-in-app.jsonl`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import { composedApplyOutcomes, memberApply, readApplyEvents } from "./apply-events";
|
|
11
|
+
|
|
12
|
+
const out = readFileSync(join(import.meta.dirname, "..", "__fixtures__", "compose", "apply-failed-in-app.jsonl"), "utf-8");
|
|
13
|
+
const modules = ["net", "app", "edge"];
|
|
14
|
+
|
|
15
|
+
describe("readApplyEvents", () => {
|
|
16
|
+
const ev = readApplyEvents(out, modules);
|
|
17
|
+
|
|
18
|
+
test("counts each module's started and completed changes and keeps its first error", () => {
|
|
19
|
+
expect(ev.started).toEqual(new Map([["net", 1], ["app", 1]]));
|
|
20
|
+
expect(ev.done).toEqual(new Map([["net", 1]]));
|
|
21
|
+
expect(ev.errored.get("app")).toBe("terraform_data.service: module.app.terraform_data.service: Creation errored after 0s");
|
|
22
|
+
expect(ev.errored.has("edge")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("an error no module's resource names is other", () => {
|
|
26
|
+
expect(ev.other).toEqual(["Provider produced inconsistent final plan: This is a bug in the provider."]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a run that failed in the second module: applied, failed, not-started", () => {
|
|
30
|
+
expect(memberApply("net", 1, ev)).toEqual({ apply: "applied", done: 1 });
|
|
31
|
+
expect(memberApply("app", 2, ev)).toEqual({ apply: "failed", done: 0 });
|
|
32
|
+
expect(memberApply("edge", 1, ev)).toEqual({ apply: "not-started", done: 0 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("some changes done is partial; none planned is no-changes", () => {
|
|
36
|
+
expect(memberApply("net", 2, ev)).toEqual({ apply: "partial", done: 1 });
|
|
37
|
+
expect(memberApply("edge", 0, ev)).toEqual({ apply: "no-changes", done: 0 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a diagnostic naming a module's resource is that module's error when no hook named one", () => {
|
|
41
|
+
const line = JSON.stringify({ type: "diagnostic", diagnostic: { severity: "error", summary: "Invalid input", detail: "first\nsecond", address: "module.edge.terraform_data.dns" } });
|
|
42
|
+
expect(readApplyEvents(line, modules).errored.get("edge")).toBe("terraform_data.dns: Error: Invalid input: first");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a module whose name starts with another's is its own", () => {
|
|
46
|
+
const line = JSON.stringify({ type: "apply_complete", hook: { resource: { addr: "module.app2.terraform_data.x" } } });
|
|
47
|
+
expect(readApplyEvents(line, ["app", "app2"]).done).toEqual(new Map([["app2", 1]]));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("composedApplyOutcomes", () => {
|
|
52
|
+
test("every member by component, the failed one with its error", () => {
|
|
53
|
+
const c = { order: ["net", "app", "edge"], modules: new Map(modules.map((m) => [m, m])) };
|
|
54
|
+
const { members, other } = composedApplyOutcomes(c, new Map([["net", 1], ["app", 2], ["edge", 1]]), out);
|
|
55
|
+
expect(members).toEqual({
|
|
56
|
+
net: { status: "applied", done: 1 },
|
|
57
|
+
app: { status: "failed", done: 0, error: "terraform_data.service: module.app.terraform_data.service: Creation errored after 0s" },
|
|
58
|
+
edge: { status: "not-started", done: 0 },
|
|
59
|
+
});
|
|
60
|
+
expect(other).toHaveLength(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How each member's part of a composed apply ended (chant#4442, epic #4419),
|
|
3
|
+
* read from the binary's `apply -json` lines: `applied`, `partial`,
|
|
4
|
+
* `failed`, `not-started` or `no-changes`, core's `GroupMemberStatus`
|
|
5
|
+
* (ws-107).
|
|
6
|
+
*
|
|
7
|
+
* Lifted from terragucci's `packages/terragucci/src/compose.ts` at c493e0e
|
|
8
|
+
* (`readApplyEvents`, `memberApply`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { GroupMemberStatus } from "@intentius/chant/components/capability";
|
|
12
|
+
import type { Composition } from "../compose";
|
|
13
|
+
import { stripModule, topModule } from "./module-address";
|
|
14
|
+
|
|
15
|
+
/** How one member's part of the apply ended. */
|
|
16
|
+
export type MemberApply = GroupMemberStatus;
|
|
17
|
+
|
|
18
|
+
/** What `apply -json` said of each module: the changes that completed, started, or errored, and the first error. */
|
|
19
|
+
export interface ApplyEvents {
|
|
20
|
+
done: Map<string, number>;
|
|
21
|
+
started: Map<string, number>;
|
|
22
|
+
errored: Map<string, string>;
|
|
23
|
+
/** Errors no module's resource names. */
|
|
24
|
+
other: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ApplyLine {
|
|
28
|
+
type?: string;
|
|
29
|
+
hook?: { resource?: { addr?: string }; error?: string };
|
|
30
|
+
diagnostic?: { severity?: string; summary?: string; detail?: string; address?: string };
|
|
31
|
+
"@message"?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read `apply -json` lines into each module's progress. A line that is not JSON is skipped. */
|
|
35
|
+
export function readApplyEvents(out: string, modules: readonly string[]): ApplyEvents {
|
|
36
|
+
const ev: ApplyEvents = { done: new Map(), started: new Map(), errored: new Map(), other: [] };
|
|
37
|
+
const known = new Set(modules);
|
|
38
|
+
const moduleOf = (addr: string | undefined): string | undefined => {
|
|
39
|
+
const m = addr !== undefined ? topModule(addr) : undefined;
|
|
40
|
+
return m !== undefined && known.has(m) && addr !== `module.${m}` ? m : undefined;
|
|
41
|
+
};
|
|
42
|
+
const bump = (map: Map<string, number>, m: string) => map.set(m, (map.get(m) ?? 0) + 1);
|
|
43
|
+
for (const line of out.split("\n")) {
|
|
44
|
+
if (!line.startsWith("{")) continue;
|
|
45
|
+
let e: ApplyLine;
|
|
46
|
+
try {
|
|
47
|
+
e = JSON.parse(line) as ApplyLine;
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const addr = e.hook?.resource?.addr;
|
|
52
|
+
const m = moduleOf(addr);
|
|
53
|
+
if (e.type === "apply_start" && m) bump(ev.started, m);
|
|
54
|
+
else if (e.type === "apply_complete" && m) bump(ev.done, m);
|
|
55
|
+
else if (e.type === "apply_errored" && m && !ev.errored.has(m)) ev.errored.set(m, `${stripModule(addr!)}: ${e.hook?.error ?? e["@message"] ?? "errored"}`);
|
|
56
|
+
else if (e.type === "diagnostic" && e.diagnostic?.severity === "error") {
|
|
57
|
+
const dm = moduleOf(e.diagnostic.address);
|
|
58
|
+
const text = `${e.diagnostic.summary ?? "Error"}${e.diagnostic.detail ? `: ${e.diagnostic.detail.split("\n")[0]}` : ""}`;
|
|
59
|
+
if (dm && !ev.errored.has(dm)) ev.errored.set(dm, `${stripModule(e.diagnostic.address!)}: Error: ${text}`);
|
|
60
|
+
else if (!dm) ev.other.push(text);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return ev;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** How a member's part of the apply ended, from its planned changes and the events. */
|
|
67
|
+
export function memberApply(m: string, planned: number, ev: ApplyEvents): { apply: MemberApply; done: number } {
|
|
68
|
+
const done = Math.min(ev.done.get(m) ?? 0, planned);
|
|
69
|
+
if (planned === 0) return { apply: "no-changes", done: 0 };
|
|
70
|
+
if (done >= planned) return { apply: "applied", done };
|
|
71
|
+
if (done > 0) return { apply: "partial", done };
|
|
72
|
+
if (ev.errored.has(m) || (ev.started.get(m) ?? 0) > 0) return { apply: "failed", done };
|
|
73
|
+
return { apply: "not-started", done };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One member's outcome of a composed apply. */
|
|
77
|
+
export interface MemberApplyOutcome {
|
|
78
|
+
status: MemberApply;
|
|
79
|
+
/** Its planned changes that completed. */
|
|
80
|
+
done: number;
|
|
81
|
+
/** The first error its part named. */
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every member's outcome of a composed apply, by component, from the
|
|
87
|
+
* `apply -json` output and each member's planned changes
|
|
88
|
+
* (`PlanCounts.changes` of its slice).
|
|
89
|
+
*/
|
|
90
|
+
export function composedApplyOutcomes(c: Pick<Composition, "order" | "modules">, planned: ReadonlyMap<string, number>, out: string): { members: Record<string, MemberApplyOutcome>; other: string[] } {
|
|
91
|
+
const ev = readApplyEvents(out, c.order.map((member) => c.modules.get(member)!));
|
|
92
|
+
const members: Record<string, MemberApplyOutcome> = {};
|
|
93
|
+
for (const member of c.order) {
|
|
94
|
+
const m = c.modules.get(member)!;
|
|
95
|
+
const { apply, done } = memberApply(m, planned.get(member) ?? 0, ev);
|
|
96
|
+
const error = ev.errored.get(m);
|
|
97
|
+
members[member] = { status: apply, done, ...(error && apply !== "applied" ? { error } : {}) };
|
|
98
|
+
}
|
|
99
|
+
return { members, other: ev.other };
|
|
100
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composed run of estates (#4445) against a real choudoufu: three
|
|
3
|
+
* estates (network, app, edge), where app tags a bucket with the id of the
|
|
4
|
+
* subnet network creates in the same apply. One plan, one apply, and the
|
|
5
|
+
* values read after it from a post-apply plan's `prior_state`.
|
|
6
|
+
*
|
|
7
|
+
* Gated on a `choudoufu` of v0.31.0 or later on PATH (choudoufu#2059) and
|
|
8
|
+
* `CHOUDOUFU_EMULATOR_ENDPOINT`, like the other acceptance suites
|
|
9
|
+
* (`../__fixtures__/ACCEPTANCE.md`), and skipped with the reason named
|
|
10
|
+
* otherwise.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
18
|
+
import type { DriverComponent } from "@intentius/chant/components/driver";
|
|
19
|
+
import { compositionFor } from "../compose";
|
|
20
|
+
import type { TerraformConfig } from "../config";
|
|
21
|
+
import { isOlderVersion, MIN_CHOUDOUFU_ESTATES_VERSION, parseChoudoufuVersion } from "../op/activities/terraform";
|
|
22
|
+
import { runBinary, type BinaryExec } from "../state/exec";
|
|
23
|
+
import { EstateRun } from "./estate";
|
|
24
|
+
|
|
25
|
+
const emulatorEndpoint = process.env.CHOUDOUFU_EMULATOR_ENDPOINT;
|
|
26
|
+
|
|
27
|
+
function choudoufuVersion(): string | undefined {
|
|
28
|
+
try {
|
|
29
|
+
return parseChoudoufuVersion(execFileSync("choudoufu", ["version"], { encoding: "utf-8" })) ?? "dev";
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const version = choudoufuVersion();
|
|
35
|
+
const missing =
|
|
36
|
+
version === undefined
|
|
37
|
+
? "choudoufu is not on PATH"
|
|
38
|
+
: version !== "dev" && isOlderVersion(version, MIN_CHOUDOUFU_ESTATES_VERSION)
|
|
39
|
+
? `choudoufu ${version} is older than v${MIN_CHOUDOUFU_ESTATES_VERSION}`
|
|
40
|
+
: !emulatorEndpoint
|
|
41
|
+
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
const made: string[] = [];
|
|
45
|
+
afterAll(() => {
|
|
46
|
+
for (const d of made) rmSync(d, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function env(): NodeJS.ProcessEnv {
|
|
50
|
+
return {
|
|
51
|
+
...process.env,
|
|
52
|
+
AWS_ENDPOINT_URL: process.env.AWS_ENDPOINT_URL ?? emulatorEndpoint,
|
|
53
|
+
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? "choudoufu-emulator",
|
|
54
|
+
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? "choudoufu-emulator",
|
|
55
|
+
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION ?? "us-east-1",
|
|
56
|
+
AWS_REGION: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PROVIDERS = 'terraform {\n required_providers {\n aws = {\n source = "hashicorp/aws"\n }\n }\n}\n\nprovider "aws" {\n region = "us-east-1"\n}\n\n';
|
|
61
|
+
const live = (estate: string, body: string): string => `terraform {\n live {\n estate = "${estate}"\n }\n}\n\n${PROVIDERS}${body}`;
|
|
62
|
+
const tf = (name: string): DriverComponent => ({ name, deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] });
|
|
63
|
+
|
|
64
|
+
describe.skipIf(missing !== undefined)(`a composed run of three estates against a real choudoufu (#4445)${missing ? `: skipped, ${missing}` : ""}`, () => {
|
|
65
|
+
it("plans all three as one, applies once, and app receives the subnet id network created", async () => {
|
|
66
|
+
const id = `chant-4445-${Date.now().toString(36)}`;
|
|
67
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-compose-estate-e2e-"));
|
|
68
|
+
made.push(dir);
|
|
69
|
+
const files: Record<string, string> = {
|
|
70
|
+
"roots/network/main.tf": live(
|
|
71
|
+
`${id}-network`,
|
|
72
|
+
'resource "aws_vpc" "main" {\n cidr_block = "10.61.0.0/16"\n}\n\nresource "aws_subnet" "a" {\n vpc_id = aws_vpc.main.id\n cidr_block = "10.61.1.0/24"\n}\n\noutput "subnet_id" {\n value = aws_subnet.a.id\n}\n',
|
|
73
|
+
),
|
|
74
|
+
"roots/app/main.tf": live(
|
|
75
|
+
`${id}-app`,
|
|
76
|
+
`data "terraform_estate_outputs" "network" {\n estate = "${id}-network"\n names = ["subnet_id"]\n}\n\nresource "aws_s3_bucket" "app" {\n bucket = "${id}-app"\n tags = {\n subnet = data.terraform_estate_outputs.network.values.subnet_id\n }\n}\n\noutput "subnet_tag" {\n value = aws_s3_bucket.app.tags["subnet"]\n}\n`,
|
|
77
|
+
),
|
|
78
|
+
"roots/edge/main.tf": live(`${id}-edge`, `resource "aws_cloudwatch_log_group" "edge" {\n name = "/${id}/edge"\n}\n`),
|
|
79
|
+
};
|
|
80
|
+
for (const [path, text] of Object.entries(files)) {
|
|
81
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
82
|
+
writeFileSync(join(dir, path), text);
|
|
83
|
+
}
|
|
84
|
+
const terraform: TerraformConfig = { binary: "choudoufu", roots: { network: { dir: "roots/network" }, app: { dir: "roots/app" }, edge: { dir: "roots/edge" } }, compose: { enabled: true } };
|
|
85
|
+
const composition = compositionFor({ projectRoot: dir, components: [tf("network"), tf("app"), tf("edge")], terraform, env: env() });
|
|
86
|
+
expect(composition.estate).toBe(true);
|
|
87
|
+
|
|
88
|
+
const calls: string[][] = [];
|
|
89
|
+
const exec: BinaryExec = (binary, args, at, e, io) => {
|
|
90
|
+
calls.push(args);
|
|
91
|
+
return runBinary(binary, args, at, e, io);
|
|
92
|
+
};
|
|
93
|
+
const run = new EstateRun({ projectRoot: dir, composition, terraform, env: env(), exec });
|
|
94
|
+
try {
|
|
95
|
+
const p = await run.plan();
|
|
96
|
+
expect(p.fallback).toBeUndefined();
|
|
97
|
+
expect(p.error).toBeUndefined();
|
|
98
|
+
expect(p.members.app!.shape.unknownInputs).toEqual(["network.subnet_id"]);
|
|
99
|
+
const a = await run.apply();
|
|
100
|
+
expect(a.error).toBeUndefined();
|
|
101
|
+
expect(a.ok).toBe(true);
|
|
102
|
+
const subnet = a.members.network!.outputs!.subnet_id;
|
|
103
|
+
expect(subnet).toMatch(/^subnet-/);
|
|
104
|
+
expect(a.members.app!.outputs!.subnet_tag).toBe(subnet);
|
|
105
|
+
expect(calls.filter((c) => c[0] === "apply")).toHaveLength(1);
|
|
106
|
+
expect(calls.some((c) => c[0] === "state" || c[0] === "force-unlock")).toBe(false);
|
|
107
|
+
for (const c of calls.filter((x) => x[0] === "show")) expect(c[c.length - 1]).toMatch(/\.tfplan$/);
|
|
108
|
+
} finally {
|
|
109
|
+
await run.release();
|
|
110
|
+
}
|
|
111
|
+
}, 300_000);
|
|
112
|
+
});
|