@intentius/chant-lexicon-terraform 0.124.52 → 0.124.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
- package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
- package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
- package/dist/__fixtures__/report/generate.d.ts +26 -0
- package/dist/__fixtures__/report/generate.d.ts.map +1 -0
- package/dist/__fixtures__/report/generate.js +58 -0
- package/dist/applied-state.d.ts +27 -0
- package/dist/applied-state.d.ts.map +1 -0
- package/dist/applied-state.js +44 -0
- package/dist/backend-config.d.ts +53 -0
- package/dist/backend-config.d.ts.map +1 -0
- package/dist/backend-config.js +136 -0
- package/dist/behaviour/kinds.d.ts.map +1 -1
- package/dist/behaviour/kinds.js +4 -0
- package/dist/behaviour/predict.d.ts.map +1 -1
- package/dist/behaviour/predict.js +6 -2
- package/dist/behaviour/reference-catalog.d.ts +16 -5
- package/dist/behaviour/reference-catalog.d.ts.map +1 -1
- package/dist/behaviour/reference-catalog.js +16 -5
- package/dist/behaviour/request.d.ts.map +1 -1
- package/dist/behaviour/request.js +3 -2
- package/dist/changed-roots.d.ts +6 -4
- package/dist/changed-roots.d.ts.map +1 -1
- package/dist/changed-roots.js +14 -18
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +4 -3
- package/dist/components/capability-plugin.d.ts +65 -3
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +197 -13
- package/dist/components/compose-group.d.ts +145 -0
- package/dist/components/compose-group.d.ts.map +1 -0
- package/dist/components/compose-group.js +407 -0
- package/dist/components/placeholder-preview.d.ts +78 -0
- package/dist/components/placeholder-preview.d.ts.map +1 -0
- package/dist/components/placeholder-preview.js +254 -0
- package/dist/compose/apply-events.d.ts +46 -0
- package/dist/compose/apply-events.d.ts.map +1 -0
- package/dist/compose/apply-events.js +77 -0
- package/dist/compose/estate.d.ts +194 -0
- package/dist/compose/estate.d.ts.map +1 -0
- package/dist/compose/estate.js +363 -0
- package/dist/compose/hcl.d.ts +99 -0
- package/dist/compose/hcl.d.ts.map +1 -0
- package/dist/compose/hcl.js +302 -0
- package/dist/compose/module-address.d.ts +24 -0
- package/dist/compose/module-address.d.ts.map +1 -0
- package/dist/compose/module-address.js +43 -0
- package/dist/compose/root.d.ts +127 -0
- package/dist/compose/root.d.ts.map +1 -0
- package/dist/compose/root.js +586 -0
- package/dist/compose/run.d.ts +183 -0
- package/dist/compose/run.d.ts.map +1 -0
- package/dist/compose/run.js +447 -0
- package/dist/compose/slice.d.ts +102 -0
- package/dist/compose/slice.d.ts.map +1 -0
- package/dist/compose/slice.js +249 -0
- package/dist/compose/state-address.d.ts +28 -0
- package/dist/compose/state-address.d.ts.map +1 -0
- package/dist/compose/state-address.js +154 -0
- package/dist/compose/state-merge.d.ts +52 -0
- package/dist/compose/state-merge.d.ts.map +1 -0
- package/dist/compose/state-merge.js +135 -0
- package/dist/compose.d.ts +179 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +482 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +7 -3
- package/dist/config.d.ts +59 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -9
- package/dist/describe-resources.d.ts +74 -12
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +374 -66
- package/dist/drift-read.d.ts +33 -0
- package/dist/drift-read.d.ts.map +1 -0
- package/dist/drift-read.js +243 -0
- package/dist/drift-record.d.ts +147 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +383 -0
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/edges.js +24 -1
- package/dist/hcl/files.d.ts +61 -0
- package/dist/hcl/files.d.ts.map +1 -0
- package/dist/hcl/files.js +221 -0
- package/dist/hcl/instances.d.ts +82 -0
- package/dist/hcl/instances.d.ts.map +1 -0
- package/dist/hcl/instances.js +390 -0
- package/dist/hcl/parse.d.ts +81 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +114 -16
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/roots.js +5 -0
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/hcl/suppressions.js +8 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -2
- package/dist/integrity.json +3 -3
- package/dist/live-plan-reasons.d.ts +21 -0
- package/dist/live-plan-reasons.d.ts.map +1 -0
- package/dist/live-plan-reasons.js +47 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/lsp/option-keys.js +2 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +6 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +5 -1
- package/dist/op/activities/live-detect.d.ts +4 -3
- package/dist/op/activities/live-detect.d.ts.map +1 -1
- package/dist/op/activities/live-detect.js +57 -32
- package/dist/op/activities/pin-rollout.d.ts +2 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.js +1 -0
- package/dist/op/activities/terraform.d.ts +153 -5
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +205 -20
- package/dist/op/activity-contracts.d.ts +27 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/activity-contracts.js +19 -1
- package/dist/op/builders.d.ts +7 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +9 -0
- package/dist/pin/edit.d.ts.map +1 -1
- package/dist/pin/edit.js +35 -0
- package/dist/pin/rollout.d.ts +2 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +14 -11
- package/dist/plan-shape.d.ts +30 -0
- package/dist/plan-shape.d.ts.map +1 -0
- package/dist/plan-shape.js +125 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -18
- package/dist/report.d.ts +15 -6
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +28 -55
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/state/azure-blob.d.ts +68 -0
- package/dist/state/azure-blob.d.ts.map +1 -0
- package/dist/state/azure-blob.js +201 -0
- package/dist/state/backend.d.ts +158 -0
- package/dist/state/backend.d.ts.map +1 -0
- package/dist/state/backend.js +406 -0
- package/dist/state/exec.d.ts +39 -0
- package/dist/state/exec.d.ts.map +1 -0
- package/dist/state/exec.js +80 -0
- package/dist/state/gcs.d.ts +62 -0
- package/dist/state/gcs.d.ts.map +1 -0
- package/dist/state/gcs.js +224 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +14 -0
- package/dist/state/locks.d.ts +105 -0
- package/dist/state/locks.d.ts.map +1 -0
- package/dist/state/locks.js +130 -0
- package/dist/state/object-store.d.ts +37 -0
- package/dist/state/object-store.d.ts.map +1 -0
- package/dist/state/object-store.js +20 -0
- package/dist/state/root-states.d.ts +131 -0
- package/dist/state/root-states.d.ts.map +1 -0
- package/dist/state/root-states.js +241 -0
- package/dist/state/s3.d.ts +68 -0
- package/dist/state/s3.d.ts.map +1 -0
- package/dist/state/s3.js +170 -0
- package/dist/state/store.d.ts +51 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +185 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +2 -1
- package/package.json +7 -3
- package/src/__fixtures__/ACCEPTANCE.md +69 -7
- package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
- package/src/__fixtures__/compose/state-app.json +66 -0
- package/src/__fixtures__/compose/state-edge.json +40 -0
- package/src/__fixtures__/compose/state-net.json +58 -0
- package/src/__fixtures__/compose/three-app.plan.json +184 -0
- package/src/__fixtures__/compose/three-composed.plan.json +509 -0
- package/src/__fixtures__/compose/three-edge.plan.json +144 -0
- package/src/__fixtures__/compose/three-net.plan.json +174 -0
- package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
- package/src/__fixtures__/compose/working-after-apply.json +145 -0
- package/src/__fixtures__/config-blocks/app/main.tf +61 -0
- package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
- package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
- package/src/__fixtures__/config-files/shadow/main.tf +3 -0
- package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
- package/src/__fixtures__/config-files/shadow/other.tf +3 -0
- package/src/__fixtures__/config-files/tf/main.tf +11 -0
- package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
- package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
- package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
- package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
- package/src/__fixtures__/drift-record/README.md +11 -0
- package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
- package/src/__fixtures__/instance-forms/app/main.tf +110 -0
- package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/main.tf +52 -0
- package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
- package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
- package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
- package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
- package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
- package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
- package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
- package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
- package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
- package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
- package/src/__fixtures__/pr-loop/README.md +50 -0
- package/src/__fixtures__/pr-loop/chant.config.json +13 -0
- package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
- package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
- package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
- package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
- package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
- package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
- package/src/__fixtures__/report/applied.show.json +51 -0
- package/src/__fixtures__/report/generate.ts +63 -0
- package/src/__fixtures__/report/plan.show.json +200 -0
- package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
- package/src/__fixtures__/report/tf-apply/report.json +338 -0
- package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
- package/src/__fixtures__/report/tf-plan/report.json +320 -0
- package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
- package/src/applied-state.ts +52 -0
- package/src/backend-config.test.ts +105 -0
- package/src/backend-config.ts +133 -0
- package/src/behaviour/kinds.ts +4 -0
- package/src/behaviour/predict.ts +5 -2
- package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
- package/src/behaviour/reference-catalog.ts +16 -5
- package/src/behaviour/request.ts +3 -2
- package/src/change-set-instances.test.ts +98 -0
- package/src/changed-roots.ts +14 -16
- package/src/commands.ts +4 -3
- package/src/components/capability-plugin.test.ts +172 -5
- package/src/components/capability-plugin.ts +212 -11
- package/src/components/change-set-graph.e2e.test.ts +11 -5
- package/src/components/compose-group.test.ts +486 -0
- package/src/components/compose-group.ts +489 -0
- package/src/components/demo-composed-roots.e2e.test.ts +440 -0
- package/src/components/member-outputs.test.ts +76 -0
- package/src/components/placeholder-preview.test.ts +147 -0
- package/src/components/placeholder-preview.ts +257 -0
- package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
- package/src/components/pr-loop.tofu.test.ts +112 -56
- package/src/compose/apply-events.test.ts +62 -0
- package/src/compose/apply-events.ts +100 -0
- package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
- package/src/compose/estate.test.ts +421 -0
- package/src/compose/estate.ts +443 -0
- package/src/compose/hcl.ts +311 -0
- package/src/compose/module-address.ts +49 -0
- package/src/compose/root.test.ts +416 -0
- package/src/compose/root.ts +592 -0
- package/src/compose/run.test.ts +440 -0
- package/src/compose/run.tofu.test.ts +176 -0
- package/src/compose/run.ts +527 -0
- package/src/compose/slice.test.ts +184 -0
- package/src/compose/slice.ts +290 -0
- package/src/compose/state-address.ts +157 -0
- package/src/compose/state-merge.test.ts +131 -0
- package/src/compose/state-merge.ts +148 -0
- package/src/compose.test.ts +248 -0
- package/src/compose.ts +627 -0
- package/src/composites/terraform-apply-op.test.ts +9 -0
- package/src/composites/terraform-apply-op.ts +7 -3
- package/src/config-file-listers.test.ts +265 -0
- package/src/config.ts +44 -9
- package/src/describe-resources.engine-facts.test.ts +412 -0
- package/src/describe-resources.live-module-keyed.test.ts +290 -0
- package/src/describe-resources.ts +416 -70
- package/src/drift-read.ts +284 -0
- package/src/drift-record.test.ts +223 -0
- package/src/drift-record.ts +455 -0
- package/src/hcl/config-blocks.test.ts +174 -0
- package/src/hcl/edges.ts +21 -1
- package/src/hcl/files.ts +234 -0
- package/src/hcl/instance-forms.test.ts +94 -0
- package/src/hcl/instances.test.ts +137 -0
- package/src/hcl/instances.ts +422 -0
- package/src/hcl/parse.test.ts +139 -1
- package/src/hcl/parse.ts +149 -16
- package/src/hcl/roots.ts +6 -0
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +76 -0
- package/src/live-plan-reasons.ts +54 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/option-keys.ts +2 -0
- package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
- package/src/op/activities/index.ts +9 -0
- package/src/op/activities/live-detect.ts +55 -26
- package/src/op/activities/pin-rollout.ts +3 -0
- package/src/op/activities/registry.test.ts +2 -1
- package/src/op/activities/terraform.test.ts +55 -0
- package/src/op/activities/terraform.ts +266 -22
- package/src/op/activity-contracts.test.ts +1 -0
- package/src/op/activity-contracts.ts +24 -1
- package/src/op/builders.ts +14 -0
- package/src/pin/bundle.test.ts +2 -0
- package/src/pin/edit.ts +30 -0
- package/src/pin/rollout.ts +16 -11
- package/src/plan-shape.test.ts +148 -0
- package/src/plan-shape.ts +128 -0
- package/src/plugin.ts +33 -19
- package/src/report.test.ts +82 -1
- package/src/report.ts +31 -54
- package/src/signal-only-root.test.ts +72 -0
- package/src/skills/chant-terraform.md +1 -1
- package/src/state/azure-blob.ts +223 -0
- package/src/state/backend-verdict.test.ts +187 -0
- package/src/state/backend.test.ts +127 -0
- package/src/state/backend.ts +488 -0
- package/src/state/exec.ts +111 -0
- package/src/state/gcs.ts +254 -0
- package/src/state/index.ts +15 -0
- package/src/state/locks.test.ts +84 -0
- package/src/state/locks.ts +201 -0
- package/src/state/object-store.ts +35 -0
- package/src/state/root-states-floci.e2e.test.ts +66 -0
- package/src/state/root-states.test.ts +462 -0
- package/src/state/root-states.ts +309 -0
- package/src/state/s3.ts +193 -0
- package/src/state/store.ts +225 -0
- package/src/terragrunt/affected.ts +2 -1
- package/src/workspace-kinds.test.ts +4 -17
- package/workspace-kinds.json +1 -24
- package/dist/workspace-member.d.ts +0 -30
- package/dist/workspace-member.d.ts.map +0 -1
- package/dist/workspace-member.js +0 -91
- package/schemas/pulumi.schema.json +0 -34
- package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
- package/src/testdata/pulumi-member/stack-export.json +0 -53
- package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
- package/src/workspace-member.test.ts +0 -89
- package/src/workspace-member.ts +0 -126
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraform-apply`'s group methods (#4447, ws-107) over three roots, a -> b
|
|
3
|
+
* -> c, where b reads a's `id` and c reads b's through `stackOutput()` in
|
|
4
|
+
* `vars`. The run is a fake `ComposedRunner`, so nothing here runs a binary;
|
|
5
|
+
* the composition is real, read from roots written to a temporary project.
|
|
6
|
+
* The real binary is `pr-loop-compose.tofu.e2e.test.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { afterAll, beforeEach, describe, expect, test } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
CapabilityRegistry,
|
|
15
|
+
isPlanMovedError,
|
|
16
|
+
type Capability,
|
|
17
|
+
type CapabilityPlan,
|
|
18
|
+
type DeployContext,
|
|
19
|
+
type GroupMember,
|
|
20
|
+
} from "@intentius/chant/components/capability";
|
|
21
|
+
import { resolveStepInput, type DriverComponent } from "@intentius/chant/components/driver";
|
|
22
|
+
import { findComponentGroups, planComponentGroup, runComponentGroup } from "@intentius/chant/components/groups";
|
|
23
|
+
import { planFanOut } from "@intentius/chant/components/fan-out";
|
|
24
|
+
import { composeChangeSet, isEmptyChangeSet, type ChangeSetPart } from "@intentius/chant/change-set";
|
|
25
|
+
import type { TerraformConfig } from "../config";
|
|
26
|
+
import type { ComposedRunApply, ComposedRunner, ComposedRunPlan } from "../compose/estate";
|
|
27
|
+
import { composeGroup, groupComposition, outsideOutputs, type ComposeGroupDeps, type GroupRunnerOptions } from "./compose-group";
|
|
28
|
+
|
|
29
|
+
const made: string[] = [];
|
|
30
|
+
afterAll(() => {
|
|
31
|
+
for (const d of made) rmSync(d, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const ROOTS: Record<string, string> = {
|
|
35
|
+
a: 'resource "terraform_data" "x" {\n input = "a"\n}\n\noutput "id" {\n value = terraform_data.x.output\n}\n',
|
|
36
|
+
b: 'variable "a_id" {\n type = string\n}\n\nresource "terraform_data" "x" {\n input = var.a_id\n}\n\noutput "id" {\n value = terraform_data.x.output\n}\n',
|
|
37
|
+
c: 'variable "b_id" {\n type = string\n}\n\nvariable "region" {\n type = string\n}\n\nresource "terraform_data" "x" {\n input = "${var.b_id} in ${var.region}"\n}\n\noutput "id" {\n value = terraform_data.x.output\n}\n',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function project(extra: Record<string, string> = {}): string {
|
|
41
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-compose-group-"));
|
|
42
|
+
made.push(dir);
|
|
43
|
+
for (const [path, text] of Object.entries({ ...Object.fromEntries(Object.entries(ROOTS).map(([r, t]) => [`roots/${r}/main.tf`, t])), ...extra })) {
|
|
44
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
45
|
+
writeFileSync(join(dir, path), text);
|
|
46
|
+
}
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const out = (stack: string, name: string) => ({ stackOutput: { stack, name } });
|
|
51
|
+
const terraform = (compose: boolean, roots: TerraformConfig["roots"] = {}): TerraformConfig =>
|
|
52
|
+
({ binary: "tofu", roots: { a: { dir: "roots/a" }, b: { dir: "roots/b" }, c: { dir: "roots/c" }, ...roots }, ...(compose ? { compose: { enabled: true } } : {}) }) as TerraformConfig;
|
|
53
|
+
|
|
54
|
+
/** The steps as declared: c also reads `net.region`, a component outside the group. */
|
|
55
|
+
const STEPS: Record<string, Record<string, unknown>> = {
|
|
56
|
+
a: { root: "a", vars: {} },
|
|
57
|
+
b: { root: "b", vars: { a_id: out("a", "id") } },
|
|
58
|
+
c: { root: "c", vars: { b_id: out("b", "id"), region: out("net", "region") } },
|
|
59
|
+
};
|
|
60
|
+
const DEPENDS: Record<string, string[]> = { a: [], b: ["a"], c: ["b", "net"] };
|
|
61
|
+
|
|
62
|
+
function members(input = true, ctx: Partial<Record<string, Partial<DeployContext>>> = {}): GroupMember<{ root: string; vars?: Record<string, unknown> }>[] {
|
|
63
|
+
return ["a", "b", "c"].map((name) => ({
|
|
64
|
+
component: name,
|
|
65
|
+
step: STEPS[name]!,
|
|
66
|
+
...(input ? { input: { root: name, vars: resolved(name) } as { root: string; vars?: Record<string, unknown> } } : {}),
|
|
67
|
+
reads: name === "a" ? [] : [{ at: `/vars/${name === "b" ? "a_id" : "b_id"}`, ref: `${name === "b" ? "a" : "b"}.id`, component: name === "b" ? "a" : "b", output: "id" }],
|
|
68
|
+
dependsOn: DEPENDS[name]!,
|
|
69
|
+
ctx: { env: "test", component: name, ...(ctx[name] ?? {}) },
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/** A step's vars as core resolves them against the outputs from outside the group: a group-mate's read is `undefined`. */
|
|
73
|
+
const resolved = (name: string): Record<string, unknown> => (name === "c" ? { b_id: undefined, region: "eu-west-1" } : name === "b" ? { a_id: undefined } : {});
|
|
74
|
+
|
|
75
|
+
const part = (root: string, actions: string[] = ["update"]): ChangeSetPart => ({
|
|
76
|
+
member: { member: root, lexicon: "terraform", planner: "tofu", status: "planned", planDigest: `jcs1-sha256:${root}`, holes: [] },
|
|
77
|
+
entries: actions.map((action) => ({ member: root, address: "terraform_data.x", type: "terraform_data", action })) as never,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** A composed plan where a's id is unknown after apply, so b's read of it and, through b, c's are unknown at plan. */
|
|
81
|
+
function composedPlan(digest = "sha256:composed-1", over: Partial<ComposedRunPlan> = {}): ComposedRunPlan {
|
|
82
|
+
const member = (root: string, unknownInputs: string[], actions?: string[]) => ({
|
|
83
|
+
member: root,
|
|
84
|
+
planDigest: `jcs1-sha256:${root}`,
|
|
85
|
+
changeSet: part(root, actions),
|
|
86
|
+
outputs: { known: {}, unknown: ["id"] },
|
|
87
|
+
shape: { member: root, entries: [{ logicalId: "terraform_data.x", action: "update" }], unknownInputs },
|
|
88
|
+
slice: {},
|
|
89
|
+
counts: { add: 0, change: 1, destroy: 0, changes: 1, destroys: 0 },
|
|
90
|
+
reads: [],
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
members: { a: member("a", []), b: member("b", ["a.id"], []), c: member("c", ["b.id"]) },
|
|
94
|
+
digest,
|
|
95
|
+
planFile: "/w/composed.tfplan",
|
|
96
|
+
text: "",
|
|
97
|
+
texts: {},
|
|
98
|
+
generated: [],
|
|
99
|
+
counts: { add: 0, change: 3, destroy: 0, changes: 3, destroys: 0 },
|
|
100
|
+
summary: "Plan: 0 to add, 3 to change, 0 to destroy.",
|
|
101
|
+
...over,
|
|
102
|
+
} as ComposedRunPlan;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const appliedAll: ComposedRunApply = {
|
|
106
|
+
ok: true,
|
|
107
|
+
members: {
|
|
108
|
+
a: { status: "applied", planned: 1, done: 1, outputs: { id: "a-new" } },
|
|
109
|
+
b: { status: "applied", planned: 1, done: 1, outputs: { id: "a-new/b" } },
|
|
110
|
+
c: { status: "applied", planned: 1, done: 1, outputs: { id: "a-new/b in eu-west-1" } },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** A fake run: what it was built with, and every call. */
|
|
115
|
+
class FakeRun implements ComposedRunner {
|
|
116
|
+
calls: string[] = [];
|
|
117
|
+
constructor(
|
|
118
|
+
readonly o: GroupRunnerOptions,
|
|
119
|
+
private readonly planned: () => ComposedRunPlan,
|
|
120
|
+
private readonly applied: () => ComposedRunApply,
|
|
121
|
+
) {}
|
|
122
|
+
async plan() {
|
|
123
|
+
this.calls.push(`plan lock=${this.o.lock}`);
|
|
124
|
+
return this.planned();
|
|
125
|
+
}
|
|
126
|
+
async apply() {
|
|
127
|
+
this.calls.push("apply");
|
|
128
|
+
return this.applied();
|
|
129
|
+
}
|
|
130
|
+
async release() {
|
|
131
|
+
this.calls.push("release");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let runs: FakeRun[];
|
|
136
|
+
let plans: ComposedRunPlan[];
|
|
137
|
+
let applied: ComposedRunApply;
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
runs = [];
|
|
140
|
+
plans = [];
|
|
141
|
+
applied = appliedAll;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function deps(dir: string, compose = true, extra: Partial<ComposeGroupDeps> = {}): ComposeGroupDeps {
|
|
145
|
+
return {
|
|
146
|
+
project: async () => ({ projectRoot: dir, terraform: terraform(compose) }),
|
|
147
|
+
runner: (o) => {
|
|
148
|
+
const r = new FakeRun(o, () => plans.shift() ?? composedPlan(), () => applied);
|
|
149
|
+
runs.push(r);
|
|
150
|
+
return r;
|
|
151
|
+
},
|
|
152
|
+
exec: async () => ({ code: 0, stdout: "OpenTofu v1.9.0", out: "" }),
|
|
153
|
+
env: {},
|
|
154
|
+
log: () => {},
|
|
155
|
+
...extra,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
describe("groupFallback (#4447)", () => {
|
|
160
|
+
test("answers off while terraform.compose is unset, so core records it and the note does not print it", async () => {
|
|
161
|
+
const g = composeGroup(deps(project(), false));
|
|
162
|
+
expect(await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false))).toEqual({ kind: "off", reason: "`terraform.compose` is not enabled" });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("takes the group when every member composes", async () => {
|
|
166
|
+
const g = composeGroup(deps(project()));
|
|
167
|
+
expect(await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false))).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a member #4438 leaves out is dropped alone with its kind and reason, so core groups the rest (#4419 ruling 1)", async () => {
|
|
171
|
+
const dir = project({ "roots/c/paths.tf": 'locals {\n here = path.root\n}\n' });
|
|
172
|
+
const g = composeGroup(deps(dir));
|
|
173
|
+
const refused = await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false));
|
|
174
|
+
expect(refused?.kind).toBe("path");
|
|
175
|
+
expect(refused?.drop).toEqual([{ component: "c", kind: "path", reason: expect.stringContaining("`c` uses `path.root` or `path.cwd`") }]);
|
|
176
|
+
// Asked again with the rest, it takes them.
|
|
177
|
+
expect(await g.groupFallback({ env: "test", components: ["a", "b"] }, members(false).filter((m) => m.component !== "c"))).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("a member under another cloud identity is dropped as identity: each root resolved as its target (#4438, #4332)", async () => {
|
|
181
|
+
const id = (role: string) => ({ plan: { aws: { role: `${role}-plan`, region: "us-east-1", audience: "sts.amazonaws.com" } }, apply: { aws: { role, region: "us-east-1", audience: "sts.amazonaws.com" } } }) as never;
|
|
182
|
+
const asked: string[] = [];
|
|
183
|
+
const g = composeGroup(
|
|
184
|
+
deps(project(), true, {
|
|
185
|
+
identity: async (projectRoot, env) => {
|
|
186
|
+
asked.push(env);
|
|
187
|
+
expect(projectRoot).toBeTruthy();
|
|
188
|
+
return (target) => ((target as readonly string[]).includes("c") ? id("role/other") : id("role/apply"));
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
const refused = await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false));
|
|
193
|
+
expect(asked).toEqual(["test"]);
|
|
194
|
+
expect(refused?.drop).toEqual([
|
|
195
|
+
{ component: "c", kind: "identity", reason: "`c` applies under another cloud identity (`ci.identity.cloud`) than `a`, `b`, and one run has one set of credentials" },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("a root matches a target glob by its key or its directory (#4332)", () => {
|
|
200
|
+
const id = (role: string) => ({ plan: { aws: { role: `${role}-plan`, region: "us-east-1", audience: "sts.amazonaws.com" } }, apply: { aws: { role, region: "us-east-1", audience: "sts.amazonaws.com" } } }) as never;
|
|
201
|
+
/** The lookup a declaration with these rules gives, in declared order, matching a glob exactly against any name. */
|
|
202
|
+
const lookupOf = (rules: Array<{ glob: string; role: string }>) => (target: string | readonly string[]) => {
|
|
203
|
+
const names = typeof target === "string" ? [target] : target;
|
|
204
|
+
const rule = rules.find((r) => names.includes(r.glob));
|
|
205
|
+
return rule ? id(rule.role) : undefined;
|
|
206
|
+
};
|
|
207
|
+
const asked = async (rules: Array<{ glob: string; role: string }>) => {
|
|
208
|
+
const seen: Array<string | readonly string[]> = [];
|
|
209
|
+
const lookup = lookupOf(rules);
|
|
210
|
+
const g = composeGroup(deps(project(), true, { identity: async () => (t) => (seen.push(t), lookup(t)) }));
|
|
211
|
+
const refused = await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false));
|
|
212
|
+
return { refused, seen };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
test("the lookup gets each root's key and its directory relative to the project", async () => {
|
|
216
|
+
const { seen } = await asked([]);
|
|
217
|
+
expect(seen).toEqual([
|
|
218
|
+
["a", "roots/a"],
|
|
219
|
+
["b", "roots/b"],
|
|
220
|
+
["c", "roots/c"],
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a glob on the root's key covers it", async () => {
|
|
225
|
+
const { refused } = await asked([{ glob: "c", role: "role/other" }, { glob: "a", role: "role/apply" }, { glob: "b", role: "role/apply" }]);
|
|
226
|
+
expect(refused?.drop?.map((d) => d.component)).toEqual(["c"]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a glob on the root's directory covers it", async () => {
|
|
230
|
+
const { refused } = await asked([{ glob: "roots/c", role: "role/other" }, { glob: "roots/a", role: "role/apply" }, { glob: "roots/b", role: "role/apply" }]);
|
|
231
|
+
expect(refused?.drop?.map((d) => d.component)).toEqual(["c"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("when the key and the directory match different rules, the first declared rule wins", async () => {
|
|
235
|
+
// c's key matches the second rule and its directory the first, so the first rule's identity applies to c, the same as a and b.
|
|
236
|
+
const { refused } = await asked([{ glob: "roots/c", role: "role/apply" }, { glob: "c", role: "role/other" }, { glob: "a", role: "role/apply" }, { glob: "b", role: "role/apply" }]);
|
|
237
|
+
expect(refused).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a root neither name matches takes no identity, as before", async () => {
|
|
241
|
+
const { refused } = await asked([{ glob: "elsewhere", role: "role/other" }]);
|
|
242
|
+
expect(refused).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("members under one cloud identity compose", async () => {
|
|
247
|
+
const g = composeGroup(deps(project(), true, { identity: async () => () => ({ plan: { aws: { role: "p", region: "us-east-1", audience: "sts.amazonaws.com" } }, apply: { aws: { role: "a", region: "us-east-1", audience: "sts.amazonaws.com" } } }) as never }));
|
|
248
|
+
expect(await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false))).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a dependsOn with no read between two members falls back as order-only", async () => {
|
|
252
|
+
const g = composeGroup(deps(project()));
|
|
253
|
+
const ms = members(false).map((m) => (m.component === "c" ? { ...m, dependsOn: ["a", "b"] } : m));
|
|
254
|
+
expect((await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, ms))?.kind).toBe("order-only");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a member given targets falls back as targets", async () => {
|
|
258
|
+
const g = composeGroup(deps(project()));
|
|
259
|
+
expect((await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false, { b: { targets: ["terraform_data.x"] } })))?.kind).toBe("targets");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a composition of estates on a choudoufu older than the estates release falls back as binary", async () => {
|
|
263
|
+
const live = (e: string, body: string) => `terraform {\n live {\n estate = "${e}"\n }\n}\n\n${body}`;
|
|
264
|
+
const dir = project(Object.fromEntries(Object.entries(ROOTS).map(([r, t]) => [`roots/${r}/main.tf`, live(r, t)])));
|
|
265
|
+
const g = composeGroup({ ...deps(dir), project: async () => ({ projectRoot: dir, terraform: { ...terraform(true), binary: "choudoufu" } }), exec: async () => ({ code: 0, stdout: "choudoufu v0.20.0", out: "" }) });
|
|
266
|
+
const refused = await g.groupFallback({ env: "test", components: ["a", "b", "c"] }, members(false));
|
|
267
|
+
expect(refused?.kind).toBe("binary");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("planGroup (#4447)", () => {
|
|
272
|
+
test("plans once without locks and releases the run before it returns", async () => {
|
|
273
|
+
const g = composeGroup(deps(project()));
|
|
274
|
+
const planned = await g.planGroup({ env: "test", components: ["a", "b", "c"] }, members());
|
|
275
|
+
expect(runs).toHaveLength(1);
|
|
276
|
+
expect(runs[0]!.calls).toEqual(["plan lock=false", "release"]);
|
|
277
|
+
expect(planned.digest).toBe("sha256:composed-1");
|
|
278
|
+
expect(planned.artifact).toEqual({ digest: "sha256:composed-1", estate: false });
|
|
279
|
+
expect(Object.keys(planned.members)).toEqual(["a", "b", "c"]);
|
|
280
|
+
// Each member's own plan, as `plan` gives one: no slice or counts ride along into the change set.
|
|
281
|
+
expect(planned.members.b).toEqual({
|
|
282
|
+
member: "b",
|
|
283
|
+
planDigest: "jcs1-sha256:b",
|
|
284
|
+
changeSet: part("b", []),
|
|
285
|
+
outputs: { known: {}, unknown: ["id"] },
|
|
286
|
+
shape: { member: "b", entries: [{ logicalId: "terraform_data.x", action: "update" }], unknownInputs: ["a.id"] },
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("hands the run the vars as written and the outside outputs core resolved", async () => {
|
|
291
|
+
const g = composeGroup(deps(project()));
|
|
292
|
+
await g.planGroup({ env: "test", components: ["a", "b", "c"] }, members());
|
|
293
|
+
expect(runs[0]!.o.vars).toEqual({ a: {}, b: STEPS.b!.vars, c: STEPS.c!.vars });
|
|
294
|
+
expect(runs[0]!.o.componentOutputs).toEqual({ net: { region: "eu-west-1" } });
|
|
295
|
+
expect(runs[0]!.o.composition.order).toEqual(["a", "b", "c"]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a member that reads an outside output not known yet makes the group wait: holes, nothing planned", async () => {
|
|
299
|
+
const g = composeGroup(deps(project()));
|
|
300
|
+
const ms = members(true, { c: { unresolvedInputs: ["net"] } });
|
|
301
|
+
const planned = await g.planGroup({ env: "test", components: ["a", "b", "c"] }, ms);
|
|
302
|
+
expect(runs).toHaveLength(0);
|
|
303
|
+
expect(planned.members.c!.changeSet!.member.holes).toEqual([{ address: "c", type: "terraform_root", reason: "inputs from net; planned again once they apply" }]);
|
|
304
|
+
expect(planned.members.a!.changeSet!.member.holes[0]!.reason).toBe("inputs from net, read by c in its group; planned again once they apply");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("a composed plan that fails throws, so core makes every member plan-failed with its error", async () => {
|
|
308
|
+
plans = [composedPlan("", { error: "the composed plan failed: Error: boom" })];
|
|
309
|
+
const g = composeGroup(deps(project()));
|
|
310
|
+
await expect(g.planGroup({ env: "test", components: ["a", "b", "c"] }, members())).rejects.toThrow("Error: boom");
|
|
311
|
+
expect(runs[0]!.calls).toEqual(["plan lock=false", "release"]);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("runGroup (#4447)", () => {
|
|
316
|
+
const run = { artifact: { digest: "sha256:composed-1", estate: false }, digest: "sha256:group", plans: {} as Record<string, CapabilityPlan> };
|
|
317
|
+
|
|
318
|
+
test("plans again under the locks, applies that same plan, releases, and gives each member's outputs", async () => {
|
|
319
|
+
const g = composeGroup(deps(project()));
|
|
320
|
+
const outcome = await g.runGroup({ env: "test", components: ["a", "b", "c"] }, run, members());
|
|
321
|
+
expect(runs[0]!.calls).toEqual(["plan lock=true", "apply", "release"]);
|
|
322
|
+
expect(outcome.members).toEqual({
|
|
323
|
+
a: { status: "applied", outputs: { id: "a-new" } },
|
|
324
|
+
b: { status: "applied", outputs: { id: "a-new/b" } },
|
|
325
|
+
c: { status: "applied", outputs: { id: "a-new/b in eu-west-1" } },
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("a plan that moved since the approval applies nothing and throws core's moved-plan mark", async () => {
|
|
330
|
+
plans = [composedPlan("sha256:composed-2")];
|
|
331
|
+
const g = composeGroup(deps(project()));
|
|
332
|
+
const err = await g.runGroup({ env: "test", components: ["a", "b", "c"] }, run, members()).catch((e: unknown) => e);
|
|
333
|
+
expect(isPlanMovedError(err)).toBe(true);
|
|
334
|
+
expect(runs[0]!.calls).toEqual(["plan lock=true", "release"]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("a lock another run holds stops the run before anything applies, and the members say why", async () => {
|
|
338
|
+
plans = [composedPlan("", { error: "b: its state is locked at roots/b/terraform.tfstate.tflock by ci@host", member: "b" })];
|
|
339
|
+
const g = composeGroup(deps(project()));
|
|
340
|
+
const outcome = await g.runGroup({ env: "test", components: ["a", "b", "c"] }, run, members());
|
|
341
|
+
expect(runs[0]!.calls).toEqual(["plan lock=true", "release"]);
|
|
342
|
+
expect(Object.values(outcome.members).map((m) => m.status)).toEqual(["not-started", "not-started", "not-started"]);
|
|
343
|
+
expect(outcome.error).toContain("its state is locked");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a failure in b leaves a applied and says so; a state kept unwritten keeps its lock and says so", async () => {
|
|
347
|
+
applied = {
|
|
348
|
+
ok: false,
|
|
349
|
+
members: {
|
|
350
|
+
a: { status: "applied", planned: 1, done: 1, outputs: { id: "a-new" } },
|
|
351
|
+
b: { status: "failed", planned: 1, done: 0, error: "terraform_data.x: Error: boom", ...({ lockHeld: true, kept: "roots/b/terraform.tfstate.chant-unwritten" } as object) },
|
|
352
|
+
c: { status: "not-started", planned: 1, done: 0 },
|
|
353
|
+
},
|
|
354
|
+
error: "the composed apply failed",
|
|
355
|
+
};
|
|
356
|
+
const g = composeGroup(deps(project()));
|
|
357
|
+
const outcome = await g.runGroup({ env: "test", components: ["a", "b", "c"] }, run, members());
|
|
358
|
+
expect(outcome.members.a).toEqual({ status: "applied", outputs: { id: "a-new" } });
|
|
359
|
+
expect(outcome.members.b!.status).toBe("failed");
|
|
360
|
+
expect(outcome.members.b!.error).toContain("Error: boom");
|
|
361
|
+
expect(outcome.members.b!.error).toContain("kept unwritten at roots/b/terraform.tfstate.chant-unwritten");
|
|
362
|
+
expect(outcome.members.c!.status).toBe("not-started");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("a member applied whose part of the state could not be shown reads its outputs from its root", async () => {
|
|
366
|
+
applied = { ok: true, members: { ...appliedAll.members, c: { status: "applied", planned: 1, done: 1 } } };
|
|
367
|
+
const g = composeGroup(deps(project(), true, { outputs: async (root) => ({ id: `${root}-from-state` }) }));
|
|
368
|
+
const outcome = await g.runGroup({ env: "test", components: ["a", "b", "c"] }, run, members());
|
|
369
|
+
expect(outcome.members.c).toEqual({ status: "applied", outputs: { id: "c-from-state" } });
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("through core's group calls (#4447, ws-107)", () => {
|
|
374
|
+
const components: DriverComponent[] = ["a", "b", "c"].map((name) => ({
|
|
375
|
+
name,
|
|
376
|
+
dependsOn: DEPENDS[name]!,
|
|
377
|
+
deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", ...STEPS[name]! }] }],
|
|
378
|
+
}));
|
|
379
|
+
const d: DriverComponent = { name: "d", dependsOn: ["c"], deploy: [{ phase: "Apply", steps: [{ kind: "shell", cmd: "true", reason: "reads c", env: { C_ID: out("c", "id") } }] }] };
|
|
380
|
+
const byName = new Map([...components, d].map((c) => [c.name, c]));
|
|
381
|
+
const group = { capability: "terraform-apply", components: ["a", "b", "c"] };
|
|
382
|
+
|
|
383
|
+
function registry(dir: string, compose = true): CapabilityRegistry {
|
|
384
|
+
const g = composeGroup(deps(dir, compose));
|
|
385
|
+
const cap: Capability<never, unknown> = {
|
|
386
|
+
kind: "terraform-apply",
|
|
387
|
+
run: async () => {
|
|
388
|
+
throw new Error("a group member is never run alone");
|
|
389
|
+
},
|
|
390
|
+
groupReads: g.groupReads as never,
|
|
391
|
+
groupFallback: g.groupFallback as never,
|
|
392
|
+
planGroup: g.planGroup as never,
|
|
393
|
+
runGroup: g.runGroup as never,
|
|
394
|
+
};
|
|
395
|
+
return new CapabilityRegistry().register(cap);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
test("core forms the group again without a member terraform-apply drops, and records its fallback (#4419 ruling 1)", async () => {
|
|
399
|
+
const dir = project({ "roots/c/paths.tf": 'locals {\n here = path.root\n}\n' });
|
|
400
|
+
const net: DriverComponent = { name: "net", deploy: [{ phase: "Apply", steps: [{ kind: "shell", cmd: "true" }] }] };
|
|
401
|
+
const all = [net, ...components];
|
|
402
|
+
const plan = planFanOut({ components: all, changed: ["a", "net"] });
|
|
403
|
+
const found = await findComponentGroups({ components: all, plan, registry: registry(dir), env: "test" });
|
|
404
|
+
expect(found.groups).toEqual([{ capability: "terraform-apply", components: ["a", "b"] }]);
|
|
405
|
+
expect(found.fallbacks).toEqual([{ component: "c", capability: "terraform-apply", kind: "path", reason: expect.stringContaining("`c` uses `path.root` or `path.cwd`") }]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe("roots wired only through terraform_remote_state (#4447)", () => {
|
|
409
|
+
const REMOTE_B =
|
|
410
|
+
'data "terraform_remote_state" "net" {\n backend = "local"\n config = {\n path = "../a/terraform.tfstate"\n }\n}\n\nresource "terraform_data" "x" {\n input = data.terraform_remote_state.net.outputs.id\n}\n';
|
|
411
|
+
// No stackOutput in vars and no dependsOn: core sees no read of its own.
|
|
412
|
+
const wired: DriverComponent[] = ["a", "b"].map((name) => ({ name, deploy: [{ phase: "Apply", steps: [{ kind: "terraform-apply", root: name }] }] }));
|
|
413
|
+
|
|
414
|
+
test("form a group, with each output b takes off the block as one of the group's reads", async () => {
|
|
415
|
+
const dir = project({ "roots/b/main.tf": REMOTE_B });
|
|
416
|
+
const plan = planFanOut({ components: wired, changed: ["a", "b"] });
|
|
417
|
+
const found = await findComponentGroups({ components: wired, plan, registry: registry(dir), env: "test" });
|
|
418
|
+
expect(found).toEqual({
|
|
419
|
+
groups: [{ capability: "terraform-apply", components: ["a", "b"], reads: [{ reader: "b", upstream: "a", ref: "a.id", output: "id" }] }],
|
|
420
|
+
fallbacks: [],
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("report nothing while terraform.compose is unset, so no group forms", async () => {
|
|
425
|
+
const dir = project({ "roots/b/main.tf": REMOTE_B });
|
|
426
|
+
const plan = planFanOut({ components: wired, changed: ["a", "b"] });
|
|
427
|
+
const found = await findComponentGroups({ components: wired, plan, registry: registry(dir, false), env: "test" });
|
|
428
|
+
expect(found).toEqual({ groups: [], fallbacks: [] });
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("b's read of a's unknown id is not a hole, and the change set does not look empty though b changes nothing (ruling 4)", async () => {
|
|
433
|
+
const planned = await planComponentGroup({
|
|
434
|
+
group,
|
|
435
|
+
components: byName,
|
|
436
|
+
registry: registry(project()),
|
|
437
|
+
env: "test",
|
|
438
|
+
ctxOf: (component) => ({ env: "test", component }),
|
|
439
|
+
outputs: { net: { region: "eu-west-1" } },
|
|
440
|
+
});
|
|
441
|
+
expect(planned.digest).toBeDefined();
|
|
442
|
+
const parts = planned.plans.flatMap((p) => ("parts" in p ? p.parts : []));
|
|
443
|
+
const b = parts.find((p) => p.member.member === "b")!;
|
|
444
|
+
expect(b.member.holes).toEqual([]);
|
|
445
|
+
expect(b.member.group).toEqual({ digest: planned.digest, reads: ["a.id"] });
|
|
446
|
+
expect(isEmptyChangeSet(composeChangeSet([b]))).toBe(false);
|
|
447
|
+
// Each member's digest has the group folded in, so one approval binds the whole composition.
|
|
448
|
+
expect(b.member.planDigest).not.toBe("jcs1-sha256:b");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("a component outside the group that reads c gets c's applied output", async () => {
|
|
452
|
+
const dir = project();
|
|
453
|
+
const reg = registry(dir);
|
|
454
|
+
const planned = await planComponentGroup({
|
|
455
|
+
group,
|
|
456
|
+
components: byName,
|
|
457
|
+
registry: reg,
|
|
458
|
+
env: "test",
|
|
459
|
+
ctxOf: (component) => ({ env: "test", component }),
|
|
460
|
+
outputs: { net: { region: "eu-west-1" } },
|
|
461
|
+
});
|
|
462
|
+
const componentOutputs: Record<string, Record<string, unknown>> = { net: { region: "eu-west-1" } };
|
|
463
|
+
const ran = await runComponentGroup({ group, components: byName, registry: reg, env: "test", ctxOf: (component) => ({ env: "test", component }), run: planned.run, componentOutputs });
|
|
464
|
+
expect(ran.results.map((r) => [r.component, r.status])).toEqual([
|
|
465
|
+
["a", "ok"],
|
|
466
|
+
["b", "ok"],
|
|
467
|
+
["c", "ok"],
|
|
468
|
+
]);
|
|
469
|
+
const { kind: _kind, ...step } = d.deploy[0]!.steps[0] as Record<string, unknown>;
|
|
470
|
+
expect(resolveStepInput(step, {}, componentOutputs)).toMatchObject({ env: { C_ID: "a-new/b in eu-west-1" } });
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("helpers (#4447)", () => {
|
|
475
|
+
test("outsideOutputs recovers what core resolved from outside the group, and nothing of the group's own", () => {
|
|
476
|
+
expect(outsideOutputs(members())).toEqual({ net: { region: "eu-west-1" } });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("groupComposition composes exactly the members, upstreams first", () => {
|
|
480
|
+
const c = groupComposition({ projectRoot: project(), terraform: terraform(true) }, members(false), {})!;
|
|
481
|
+
expect(groupComposition({ projectRoot: project(), terraform: terraform(false) }, members(false), {})).toBeUndefined();
|
|
482
|
+
expect(c.fallback).toBeUndefined();
|
|
483
|
+
expect(c.order).toEqual(["a", "b", "c"]);
|
|
484
|
+
expect(c.reads.get("b")).toEqual([{ via: "vars", upstream: "a", variable: "a_id", output: "id" }]);
|
|
485
|
+
});
|
|
486
|
+
});
|