@intentius/chant-lexicon-terraform 0.102.0 → 0.104.0
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/dist/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
- package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
- package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
- package/dist/change-set.d.ts +67 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/changed-roots.d.ts +30 -0
- package/dist/changed-roots.d.ts.map +1 -0
- package/dist/commands.d.ts +26 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/components/capability-plugin.d.ts +54 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
- package/dist/hcl/descend.d.ts +1 -1
- package/dist/hcl/descend.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +2 -2
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +29 -1
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +1 -1
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +13 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +27 -1
- package/dist/lint/post-synth/module-source.d.ts.map +1 -1
- package/dist/lint/post-synth/terragrunt.d.ts +37 -0
- package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
- package/dist/lint/post-synth/tf002.d.ts +3 -1
- package/dist/lint/post-synth/tf002.d.ts.map +1 -1
- package/dist/lint/post-synth/tf038.d.ts +22 -0
- package/dist/lint/post-synth/tf038.d.ts.map +1 -0
- package/dist/lint/post-synth/tf039.d.ts +23 -0
- package/dist/lint/post-synth/tf039.d.ts.map +1 -0
- package/dist/lint/post-synth/tf040.d.ts +30 -0
- package/dist/lint/post-synth/tf040.d.ts.map +1 -0
- package/dist/lint/post-synth/tf041.d.ts +20 -0
- package/dist/lint/post-synth/tf041.d.ts.map +1 -0
- package/dist/lint/post-synth/tf042.d.ts +18 -0
- package/dist/lint/post-synth/tf042.d.ts.map +1 -0
- package/dist/lint/post-synth/tf043.d.ts +17 -0
- package/dist/lint/post-synth/tf043.d.ts.map +1 -0
- package/dist/lint/post-synth/tf044.d.ts +17 -0
- package/dist/lint/post-synth/tf044.d.ts.map +1 -0
- package/dist/lint/post-synth/tf045.d.ts +22 -0
- package/dist/lint/post-synth/tf045.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/TF038.md +11 -0
- package/dist/okf/rules/TF039.md +11 -0
- package/dist/okf/rules/TF040.md +11 -0
- package/dist/okf/rules/TF041.md +11 -0
- package/dist/okf/rules/TF042.md +11 -0
- package/dist/okf/rules/TF043.md +11 -0
- package/dist/okf/rules/TF044.md +11 -0
- package/dist/okf/rules/TF045.md +11 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.d.ts +46 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +24 -50
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +48 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/pin/edit-ts.d.ts +21 -0
- package/dist/pin/edit-ts.d.ts.map +1 -0
- package/dist/pin/edit.d.ts +52 -0
- package/dist/pin/edit.d.ts.map +1 -0
- package/dist/pin/forge.d.ts +39 -0
- package/dist/pin/forge.d.ts.map +1 -0
- package/dist/pin/index.d.ts +22 -0
- package/dist/pin/index.d.ts.map +1 -0
- package/dist/pin/rollout.d.ts +116 -0
- package/dist/pin/rollout.d.ts.map +1 -0
- package/dist/pin/scan.d.ts +54 -0
- package/dist/pin/scan.d.ts.map +1 -0
- package/dist/pin/source.d.ts +93 -0
- package/dist/pin/source.d.ts.map +1 -0
- package/dist/pin/waves.d.ts +66 -0
- package/dist/pin/waves.d.ts.map +1 -0
- package/dist/plan-digest.d.ts +59 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/module-source.ts +56 -1
- package/dist/rules/terragrunt.ts +76 -0
- package/dist/rules/tf002.ts +1 -1
- package/dist/rules/tf038.ts +71 -0
- package/dist/rules/tf039.ts +64 -0
- package/dist/rules/tf040.ts +76 -0
- package/dist/rules/tf041.ts +56 -0
- package/dist/rules/tf042.ts +42 -0
- package/dist/rules/tf043.ts +49 -0
- package/dist/rules/tf044.ts +51 -0
- package/dist/rules/tf045.ts +47 -0
- package/dist/terragrunt/affected.d.ts +119 -0
- package/dist/terragrunt/affected.d.ts.map +1 -0
- package/dist/terragrunt/index.d.ts +11 -0
- package/dist/terragrunt/index.d.ts.map +1 -0
- package/dist/terragrunt/mocks.d.ts +147 -0
- package/dist/terragrunt/mocks.d.ts.map +1 -0
- package/dist/terragrunt/run.d.ts +130 -0
- package/dist/terragrunt/run.d.ts.map +1 -0
- package/dist/terragrunt/units.d.ts +103 -0
- package/dist/terragrunt/units.d.ts.map +1 -0
- package/dist/terragrunt/wave.d.ts +89 -0
- package/dist/terragrunt/wave.d.ts.map +1 -0
- package/package.json +12 -2
- package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
- package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
- package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
- package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
- package/src/__fixtures__/change-set/terraform.plan.json +589 -0
- package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/tofu.plan.json +588 -0
- package/src/__fixtures__/change-set/v1.tf.txt +21 -0
- package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
- package/src/__fixtures__/change-set/v2.tf.txt +21 -0
- package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
- package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
- package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
- package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
- package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
- package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
- package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
- package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
- package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
- package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/record.ts +56 -0
- package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
- package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
- package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
- package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
- package/src/change-set-combined-run.test.ts +156 -0
- package/src/change-set.test.ts +151 -0
- package/src/change-set.ts +273 -0
- package/src/changed-roots.test.ts +58 -0
- package/src/changed-roots.ts +102 -0
- package/src/commands.ts +112 -0
- package/src/components/capability-plugin.test.ts +91 -0
- package/src/components/capability-plugin.ts +131 -0
- package/src/components/pr-loop.tofu.test.ts +244 -0
- package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
- package/src/composites/terraform-pin-rollout-op.ts +60 -0
- package/src/hcl/descend.ts +1 -1
- package/src/hcl/edges.ts +2 -2
- package/src/hcl/parse.test.ts +25 -0
- package/src/hcl/parse.ts +91 -6
- package/src/hcl/roots.ts +1 -1
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +18 -0
- package/src/lint/audit-catalog.ts +82 -0
- package/src/lint/audit.test.ts +3 -1
- package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
- package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
- package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
- package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/module-source.test.ts +42 -1
- package/src/lint/post-synth/module-source.ts +56 -1
- package/src/lint/post-synth/terragrunt.ts +76 -0
- package/src/lint/post-synth/tf002.ts +1 -1
- package/src/lint/post-synth/tf038.test.ts +51 -0
- package/src/lint/post-synth/tf038.ts +71 -0
- package/src/lint/post-synth/tf039.test.ts +40 -0
- package/src/lint/post-synth/tf039.ts +64 -0
- package/src/lint/post-synth/tf040.test.ts +55 -0
- package/src/lint/post-synth/tf040.ts +76 -0
- package/src/lint/post-synth/tf041.test.ts +50 -0
- package/src/lint/post-synth/tf041.ts +56 -0
- package/src/lint/post-synth/tf042.test.ts +19 -0
- package/src/lint/post-synth/tf042.ts +42 -0
- package/src/lint/post-synth/tf043.test.ts +65 -0
- package/src/lint/post-synth/tf043.ts +49 -0
- package/src/lint/post-synth/tf044.test.ts +27 -0
- package/src/lint/post-synth/tf044.ts +51 -0
- package/src/lint/post-synth/tf045.test.ts +30 -0
- package/src/lint/post-synth/tf045.ts +47 -0
- package/src/op/activities/index.ts +4 -0
- package/src/op/activities/pin-rollout.ts +77 -0
- package/src/op/activities/terraform.test.ts +13 -0
- package/src/op/activities/terraform.ts +46 -86
- package/src/op/activity-contracts.test.ts +4 -1
- package/src/op/activity-contracts.ts +42 -0
- package/src/op/builders.ts +13 -0
- package/src/pin/bundle.test.ts +53 -0
- package/src/pin/edit-ts.test.ts +39 -0
- package/src/pin/edit-ts.ts +77 -0
- package/src/pin/edit.test.ts +199 -0
- package/src/pin/edit.ts +157 -0
- package/src/pin/forge.ts +85 -0
- package/src/pin/index.ts +22 -0
- package/src/pin/rollout.test.ts +232 -0
- package/src/pin/rollout.ts +451 -0
- package/src/pin/scan.ts +309 -0
- package/src/pin/source.ts +206 -0
- package/src/pin/waves.test.ts +101 -0
- package/src/pin/waves.ts +125 -0
- package/src/plan-digest.ts +99 -0
- package/src/plan-summary.test.ts +154 -0
- package/src/plugin.ts +25 -2
- package/src/terragrunt/affected.acceptance.test.ts +83 -0
- package/src/terragrunt/affected.test.ts +177 -0
- package/src/terragrunt/affected.ts +348 -0
- package/src/terragrunt/index.ts +11 -0
- package/src/terragrunt/mocks.acceptance.test.ts +105 -0
- package/src/terragrunt/mocks.test.ts +309 -0
- package/src/terragrunt/mocks.ts +324 -0
- package/src/terragrunt/run.ts +369 -0
- package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
- package/src/terragrunt/terragrunt.test.ts +332 -0
- package/src/terragrunt/units.ts +235 -0
- package/src/terragrunt/wave.ts +181 -0
package/src/pin/waves.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wave plan for a pin-bump rollout (#3189): which roots move in which PR.
|
|
3
|
+
*
|
|
4
|
+
* Two sources give the same shape, an ordered list of root lists:
|
|
5
|
+
*
|
|
6
|
+
* - **Declared canaries, then dependency order.** The canaries form wave 1.
|
|
7
|
+
* Every other root lands in the earliest wave after every root it depends
|
|
8
|
+
* on. That is the Kahn layering `chant components fan-out` uses
|
|
9
|
+
* (`resolveComponentGraph`, `@intentius/chant/components/layers`), and the
|
|
10
|
+
* order #3049 gives its gated waves. A canary that depends on a root outside
|
|
11
|
+
* the canaries is refused, since wave 1 would apply before what it reads.
|
|
12
|
+
* - **choudoufu's wave planner** (`choudoufu live-waves -json`, choudoufu#1754).
|
|
13
|
+
* It reads cross-estate references from the estates' own configuration, puts
|
|
14
|
+
* `-canary` roots in wave 1, and emits each wave's roots on their own, so
|
|
15
|
+
* {@link wavesFromChoudoufu} takes its document as it is.
|
|
16
|
+
*
|
|
17
|
+
* A root's dependencies come from the caller (`dependsOn`) and, for a
|
|
18
|
+
* Terragrunt unit, from its `dependency` and `dependencies` blocks
|
|
19
|
+
* ({@link terragruntDependencies}). A dependency on a root outside the
|
|
20
|
+
* rollout orders nothing: it is not moving.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { posix } from "node:path";
|
|
24
|
+
import type { Hcl2Json } from "@intentius/chant/terraform/hcl2json";
|
|
25
|
+
import { kahnLayers } from "@intentius/chant/components/layers";
|
|
26
|
+
|
|
27
|
+
/** One root in a rollout. `root` is its directory, relative to the repository, with `/` separators. */
|
|
28
|
+
export interface PinRoot {
|
|
29
|
+
root: string;
|
|
30
|
+
/** Roots this one reads, by directory. They land in an earlier wave. */
|
|
31
|
+
dependsOn?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* The TypeScript file a generated root is built from. The pin edit then
|
|
34
|
+
* goes to the module declaration there rather than to the `.tf` chant
|
|
35
|
+
* writes (`./edit-ts.ts`).
|
|
36
|
+
*/
|
|
37
|
+
tsSource?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One wave: its number from 1, whether the declared canaries form it, and its roots, sorted. */
|
|
41
|
+
export interface PinWave {
|
|
42
|
+
wave: number;
|
|
43
|
+
canary: boolean;
|
|
44
|
+
roots: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Thrown when the roots cannot be split into waves. The message names the roots. */
|
|
48
|
+
export class PinWavePlanError extends Error {
|
|
49
|
+
constructor(message: string) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "PinWavePlanError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Canaries first, then dependency order. */
|
|
56
|
+
export function planPinWaves(roots: readonly PinRoot[], canaries: readonly string[] = []): PinWave[] {
|
|
57
|
+
const names = new Set(roots.map((r) => r.root));
|
|
58
|
+
for (const c of canaries) {
|
|
59
|
+
if (!names.has(c)) throw new PinWavePlanError(`canary ${c} is not a root of this rollout (roots: ${[...names].sort().join(", ") || "none"})`);
|
|
60
|
+
}
|
|
61
|
+
const canarySet = new Set(canaries);
|
|
62
|
+
const deps = new Map<string, Set<string>>();
|
|
63
|
+
for (const r of roots) deps.set(r.root, new Set((r.dependsOn ?? []).filter((d) => names.has(d) && d !== r.root)));
|
|
64
|
+
|
|
65
|
+
for (const c of canarySet) {
|
|
66
|
+
const outside = [...deps.get(c)!].filter((d) => !canarySet.has(d));
|
|
67
|
+
if (outside.length > 0) throw new PinWavePlanError(`canary ${c} depends on ${outside.join(", ")}, which is not a canary, so wave 1 would move it first`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const waves: PinWave[] = [];
|
|
71
|
+
if (canarySet.size > 0) waves.push({ wave: 1, canary: true, roots: [...canarySet].sort() });
|
|
72
|
+
const rest = new Map([...deps].filter(([n]) => !canarySet.has(n)).map(([n, d]) => [n, new Set([...d].filter((x) => !canarySet.has(x)))]));
|
|
73
|
+
const layered = kahnLayers(rest);
|
|
74
|
+
if (layered.cycle) throw new PinWavePlanError(`dependency cycle among roots: ${layered.cycle.join(", ")}`);
|
|
75
|
+
for (const roots of layered.waves) waves.push({ wave: waves.length + 1, canary: false, roots });
|
|
76
|
+
return waves;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The waves of a `choudoufu live-waves -json` document (format version 1).
|
|
81
|
+
* `prefix` is the directory choudoufu ran in, relative to the repository,
|
|
82
|
+
* when its roots are named relative to that rather than to the repository.
|
|
83
|
+
*/
|
|
84
|
+
export function wavesFromChoudoufu(doc: unknown, prefix = ""): PinWave[] {
|
|
85
|
+
const d = doc as { format_version?: unknown; waves?: unknown };
|
|
86
|
+
if (d?.format_version !== "1") throw new PinWavePlanError(`not a choudoufu live-waves document of format version 1 (format_version: ${JSON.stringify(d?.format_version)})`);
|
|
87
|
+
if (!Array.isArray(d.waves)) throw new PinWavePlanError("the live-waves document has no waves");
|
|
88
|
+
return d.waves.map((w: { wave?: unknown; canary?: unknown; roots?: unknown }, i) => {
|
|
89
|
+
if (w.wave !== i + 1 || !Array.isArray(w.roots) || w.roots.some((r) => typeof r !== "string")) {
|
|
90
|
+
throw new PinWavePlanError(`live-waves wave ${i + 1} is malformed`);
|
|
91
|
+
}
|
|
92
|
+
return { wave: i + 1, canary: w.canary === true, roots: (w.roots as string[]).map((r) => (prefix ? posix.join(prefix, r) : posix.normalize(r))).sort() };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Keep only the roots in `keep`, renumbering the waves and dropping any left
|
|
98
|
+
* empty. A wave plan from choudoufu covers every root it was given; the
|
|
99
|
+
* rollout moves only the roots that call the module at the old or new pin.
|
|
100
|
+
*/
|
|
101
|
+
export function restrictWaves(waves: readonly PinWave[], keep: ReadonlySet<string>): PinWave[] {
|
|
102
|
+
const out: PinWave[] = [];
|
|
103
|
+
for (const w of waves) {
|
|
104
|
+
const roots = w.roots.filter((r) => keep.has(r));
|
|
105
|
+
if (roots.length > 0) out.push({ wave: out.length + 1, canary: w.canary, roots });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The roots a Terragrunt unit depends on: each `dependency` block's
|
|
112
|
+
* `config_path` and the `dependencies` block's `paths`, resolved against the
|
|
113
|
+
* unit's directory. A path that is an expression is skipped, since it names
|
|
114
|
+
* no directory until Terragrunt evaluates it.
|
|
115
|
+
*/
|
|
116
|
+
export async function terragruntDependencies(root: string, text: string, parser: Hcl2Json): Promise<string[]> {
|
|
117
|
+
const tree = (await parser.parse(posix.join(root, "terragrunt.hcl"), text)) as {
|
|
118
|
+
dependency?: Record<string, Array<{ config_path?: unknown }>>;
|
|
119
|
+
dependencies?: Array<{ paths?: unknown }>;
|
|
120
|
+
};
|
|
121
|
+
const paths: string[] = [];
|
|
122
|
+
for (const blocks of Object.values(tree.dependency ?? {})) for (const b of blocks) if (typeof b.config_path === "string") paths.push(b.config_path);
|
|
123
|
+
for (const b of tree.dependencies ?? []) if (Array.isArray(b.paths)) for (const p of b.paths) if (typeof p === "string") paths.push(p);
|
|
124
|
+
return [...new Set(paths.filter((p) => !p.includes("${")).map((p) => posix.normalize(posix.join(root, p))))].sort();
|
|
125
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A terraform plan's identity (#2300), kept apart from the activities so the
|
|
3
|
+
* change-set adapter (`../change-set.ts`, #3181) can import it without the
|
|
4
|
+
* config loader, the HCL parse or anything else the activities reach. It
|
|
5
|
+
* imports chant's `lifecycle/plan-digest` subpath and nothing more, which is
|
|
6
|
+
* what lets terragucci bundle it into one file (#3421).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One resource's proposed change, as a plan digest sees it (#2300) — the
|
|
13
|
+
* projection of a `terraform show -json` `resource_changes` entry that
|
|
14
|
+
* survives into {@link terraformPlanDigest}.
|
|
15
|
+
*/
|
|
16
|
+
export interface TerraformPlannedChange {
|
|
17
|
+
address: unknown;
|
|
18
|
+
mode: unknown;
|
|
19
|
+
type: unknown;
|
|
20
|
+
name: unknown;
|
|
21
|
+
index: unknown;
|
|
22
|
+
provider_name: unknown;
|
|
23
|
+
deposed: unknown;
|
|
24
|
+
actions: unknown;
|
|
25
|
+
before: unknown;
|
|
26
|
+
after: unknown;
|
|
27
|
+
after_unknown: unknown;
|
|
28
|
+
replace_paths: unknown;
|
|
29
|
+
importing: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The change set a `terraform show -json` plan describes, canonicalised
|
|
34
|
+
* (#2300): every proposed change, at its address, with the values that would
|
|
35
|
+
* be written, sorted so two renderings of the same plan project identically.
|
|
36
|
+
*
|
|
37
|
+
* Everything else in the document is dropped, and the drops are the point —
|
|
38
|
+
* see `packages/core/src/lifecycle/plan-digest.ts` for the general rule. Here
|
|
39
|
+
* that means `timestamp` and `terraform_version` (when the plan was taken and
|
|
40
|
+
* by which binary), `format_version` (how it was rendered), `prior_state` and
|
|
41
|
+
* `planned_values` (restatements of the same change set from another angle),
|
|
42
|
+
* `configuration` (the source that produced it, which can be refactored
|
|
43
|
+
* without changing a single proposed action) and `checks` / `relevant_
|
|
44
|
+
* attributes` (diagnostics about the plan, not the plan). What is left is
|
|
45
|
+
* what applying it would do.
|
|
46
|
+
*/
|
|
47
|
+
export function terraformPlanChangeSet(planJson: unknown): {
|
|
48
|
+
resourceChanges: TerraformPlannedChange[];
|
|
49
|
+
outputChanges: unknown;
|
|
50
|
+
actionInvocations?: unknown[];
|
|
51
|
+
} {
|
|
52
|
+
const doc = planJson as { resource_changes?: unknown; output_changes?: unknown; action_invocations?: unknown } | null | undefined;
|
|
53
|
+
const raw = Array.isArray(doc?.resource_changes) ? doc.resource_changes : [];
|
|
54
|
+
const resourceChanges = raw.map((entry): TerraformPlannedChange => {
|
|
55
|
+
const e = (entry ?? {}) as Record<string, unknown>;
|
|
56
|
+
const change = (e.change ?? {}) as Record<string, unknown>;
|
|
57
|
+
return {
|
|
58
|
+
address: e.address,
|
|
59
|
+
mode: e.mode,
|
|
60
|
+
type: e.type,
|
|
61
|
+
name: e.name,
|
|
62
|
+
index: e.index,
|
|
63
|
+
provider_name: e.provider_name,
|
|
64
|
+
deposed: e.deposed,
|
|
65
|
+
actions: change.actions,
|
|
66
|
+
before: change.before,
|
|
67
|
+
after: change.after,
|
|
68
|
+
after_unknown: change.after_unknown,
|
|
69
|
+
replace_paths: change.replace_paths,
|
|
70
|
+
importing: change.importing,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
// terraform already emits these in address order, but nothing in the format
|
|
74
|
+
// promises it, and a digest that depends on the emitter's iteration order
|
|
75
|
+
// would refuse a plan that is identical in every way that matters.
|
|
76
|
+
resourceChanges.sort((a, b) =>
|
|
77
|
+
`${String(a.address)}\u0000${String(a.deposed ?? "")}`.localeCompare(
|
|
78
|
+
`${String(b.address)}\u0000${String(b.deposed ?? "")}`,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
// Terraform 1.14 `action` blocks: side effects an apply runs. Present only
|
|
82
|
+
// when the plan has some, so the digest of a plan without them is unchanged.
|
|
83
|
+
const invocations = Array.isArray(doc?.action_invocations) ? [...doc.action_invocations].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))) : [];
|
|
84
|
+
return { resourceChanges, outputChanges: doc?.output_changes ?? {}, ...(invocations.length > 0 ? { actionInvocations: invocations } : {}) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The identity of a terraform plan (#2300): {@link terraformPlanChangeSet}
|
|
89
|
+
* hashed. This is what a `TerraformApplyOp` gate binds its approval to, and
|
|
90
|
+
* what a later run recomputes and compares.
|
|
91
|
+
*
|
|
92
|
+
* Taken over the `show -json` rendering rather than the plan file's bytes on
|
|
93
|
+
* purpose: the file is an opaque, version-stamped archive, and two runs of
|
|
94
|
+
* `plan -out` over an unchanged root produce different bytes. The rendering
|
|
95
|
+
* is the same plan both times.
|
|
96
|
+
*/
|
|
97
|
+
export function terraformPlanDigest(planJson: unknown): string {
|
|
98
|
+
return computePlanDigest("terraform-plan", terraformPlanChangeSet(planJson));
|
|
99
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grouped plan summary (#3188) over terraform-family change sets.
|
|
3
|
+
*
|
|
4
|
+
* `packages/core/src/__fixtures__/plan-summary/normalization-vectors.json`
|
|
5
|
+
* is the rule table chant and choudoufu both run (the ruling on #3188 and
|
|
6
|
+
* choudoufu#1753). Its root pairs are choudoufu set-plan roots, read here
|
|
7
|
+
* through `choudoufuSetPlanParts` exactly as a real set plan is; its
|
|
8
|
+
* instance pairs are two `resource_changes` of one plan. The table is proved
|
|
9
|
+
* red two ways: a grouper that compares raw changes fails every must-group
|
|
10
|
+
* row, and one that blanks every string fails the must-not rows the file
|
|
11
|
+
* names.
|
|
12
|
+
*
|
|
13
|
+
* `__fixtures__/plan-summary/largeset-n5-bump.json` is choudoufu's real set
|
|
14
|
+
* plan of choudoufu#1750's module bump at N=5 against floci
|
|
15
|
+
* (`internal/live/plansummary/testdata/largeset-n5-bump.json`, choudoufu
|
|
16
|
+
* 8f08b6cbb7). e04 is the planted outlier. Goldens regenerate with
|
|
17
|
+
* `UPDATE_GOLDEN=1`; `packages/core/src/plan-summary.test.ts` validates the
|
|
18
|
+
* JSON one against the schema.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { describe, expect, test } from "vitest";
|
|
24
|
+
import { composeChangeSet } from "@intentius/chant/change-set";
|
|
25
|
+
import { groupChangeSet, renderPlanSummaryMarkdown, renderPlanSummaryText } from "@intentius/chant/plan-summary";
|
|
26
|
+
import { choudoufuSetPlanParts, terraformChangeSetPart } from "./change-set";
|
|
27
|
+
|
|
28
|
+
const DIR = join(import.meta.dirname, "__fixtures__", "plan-summary");
|
|
29
|
+
const VECTORS = join(import.meta.dirname, "..", "..", "..", "packages", "core", "src", "__fixtures__", "plan-summary", "normalization-vectors.json");
|
|
30
|
+
|
|
31
|
+
function golden(name: string, text: string): void {
|
|
32
|
+
const path = join(DIR, name);
|
|
33
|
+
if (process.env.UPDATE_GOLDEN) writeFileSync(path, text);
|
|
34
|
+
expect(text).toBe(readFileSync(path, "utf-8"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Json = Record<string, any>;
|
|
38
|
+
|
|
39
|
+
interface Pair {
|
|
40
|
+
name: string;
|
|
41
|
+
group: boolean;
|
|
42
|
+
a: Json;
|
|
43
|
+
b: Json;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const vectors = JSON.parse(readFileSync(VECTORS, "utf-8")) as {
|
|
47
|
+
format: string;
|
|
48
|
+
rootPairs: Pair[];
|
|
49
|
+
instancePairs: Pair[];
|
|
50
|
+
overEagerFalselyGroups: string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
|
54
|
+
|
|
55
|
+
/** Whether the summary puts two roots of one set plan in one group. */
|
|
56
|
+
function rootsGroup(a: Json, b: Json): boolean {
|
|
57
|
+
const doc = composeChangeSet(choudoufuSetPlanParts({ document: { roots: [a, b] } }));
|
|
58
|
+
const s = groupChangeSet(doc);
|
|
59
|
+
return s.failed.length === 0 && s.groups.length === 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether the summary puts two instances of one plan in one group. */
|
|
63
|
+
function instancesGroup(a: Json, b: Json): boolean {
|
|
64
|
+
const doc = composeChangeSet([terraformChangeSetPart({ member: "root", plan: { format_version: "1.2", resource_changes: [a, b] } })]);
|
|
65
|
+
return groupChangeSet(doc).groups.length === 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Grouping by the raw change set: equal bytes or nothing. */
|
|
69
|
+
const naiveRootsGroup = (a: Json, b: Json): boolean => JSON.stringify(a.plan.resource_changes) === JSON.stringify(b.plan.resource_changes);
|
|
70
|
+
|
|
71
|
+
/** The normalizer this must not be: every string in before and after blanked first. */
|
|
72
|
+
function blankStrings(root: Json): Json {
|
|
73
|
+
const blank = (v: unknown): unknown =>
|
|
74
|
+
typeof v === "string" ? "x" : Array.isArray(v) ? v.map(blank) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, e]) => [k, blank(e)])) : v;
|
|
75
|
+
const r = clone(root);
|
|
76
|
+
for (const rc of r.plan.resource_changes) {
|
|
77
|
+
if (rc.change.before) rc.change.before = blank(rc.change.before);
|
|
78
|
+
if (rc.change.after) rc.change.after = blank(rc.change.after);
|
|
79
|
+
}
|
|
80
|
+
return r;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("the shared normalization vectors", () => {
|
|
84
|
+
test("are the format this test reads, with both kinds of pair", () => {
|
|
85
|
+
expect(vectors.format).toBe("plan-summary-normalization-vectors/1");
|
|
86
|
+
for (const rows of [vectors.rootPairs, vectors.instancePairs]) {
|
|
87
|
+
expect(rows.some((r) => r.group)).toBe(true);
|
|
88
|
+
expect(rows.some((r) => !r.group)).toBe(true);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test.each(vectors.rootPairs.map((r) => [r.name, r] as const))("roots: %s", (_, row) => {
|
|
93
|
+
expect(rootsGroup(row.a, row.b)).toBe(row.group);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test.each(vectors.instancePairs.map((r) => [r.name, r] as const))("instances: %s", (_, row) => {
|
|
97
|
+
expect(instancesGroup(row.a, row.b)).toBe(row.group);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("red first: a raw hash keeps every must-group pair apart", () => {
|
|
101
|
+
const grouped = vectors.rootPairs.filter((r) => r.group && naiveRootsGroup(r.a, r.b)).map((r) => r.name);
|
|
102
|
+
expect(grouped).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("red first: blanking every string falsely groups exactly the named must-not pairs", () => {
|
|
106
|
+
const falselyGrouped = vectors.rootPairs.filter((r) => !r.group && rootsGroup(blankStrings(r.a), blankStrings(r.b))).map((r) => r.name);
|
|
107
|
+
expect(falselyGrouped.sort()).toEqual([...vectors.overEagerFalselyGroups].sort());
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("choudoufu#1750's module bump at N=5", () => {
|
|
112
|
+
const document = JSON.parse(readFileSync(join(DIR, "largeset-n5-bump.json"), "utf-8")) as Json;
|
|
113
|
+
const doc = composeChangeSet(choudoufuSetPlanParts({ document }));
|
|
114
|
+
const summary = groupChangeSet(doc);
|
|
115
|
+
|
|
116
|
+
test("is one group plus the planted outlier, which reads as the group's change plus its extra queue", () => {
|
|
117
|
+
expect(summary.groups.map((g) => g.units)).toEqual([["estates/e01", "estates/e02", "estates/e03", "estates/e05"], ["estates/e04"]]);
|
|
118
|
+
const [base, outlier] = summary.groups;
|
|
119
|
+
expect(outlier.outlier).toBe(true);
|
|
120
|
+
expect(outlier.extends).toBe(base.id);
|
|
121
|
+
expect(outlier.plus?.map((l) => l.line)).toEqual(["~ module.shared.aws_sqs_queue.extra: tags, tags_all"]);
|
|
122
|
+
expect(summary.destroys).toEqual([]);
|
|
123
|
+
expect(summary.failed).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("keeps its group ids when the same change is planned again", () => {
|
|
127
|
+
const again = groupChangeSet(composeChangeSet(choudoufuSetPlanParts({ document: clone(document) }).reverse()));
|
|
128
|
+
expect(again.groups.map((g) => g.id)).toEqual(summary.groups.map((g) => g.id));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("renders its goldens", () => {
|
|
132
|
+
golden("largeset-n5.summary.golden.json", JSON.stringify(summary, null, 2) + "\n");
|
|
133
|
+
golden("largeset-n5.summary.golden.txt", renderPlanSummaryText(summary));
|
|
134
|
+
golden("largeset-n5.summary.golden.md", renderPlanSummaryMarkdown(summary));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("one root's for_each expansion", () => {
|
|
139
|
+
test("groups instances whose changes differ only in their own key, and names each destroy", () => {
|
|
140
|
+
const changes = ["a", "b", "c"].map((k) => ({
|
|
141
|
+
address: `aws_s3_bucket.logs["${k}"]`,
|
|
142
|
+
change: { actions: ["update"], before: { bucket: `logs-${k}`, tags: { Name: `logs-${k}`, v: "1" } }, after: { bucket: `logs-${k}`, tags: { Name: `logs-${k}`, v: "2" } } },
|
|
143
|
+
}));
|
|
144
|
+
changes.push({ address: 'aws_s3_bucket.logs["d"]', change: { actions: ["delete", "create"], before: { bucket: "logs-d", tags: { Name: "logs-d", v: "1" } }, after: { bucket: "logs-d2", tags: { Name: "logs-d", v: "2" } } } });
|
|
145
|
+
const doc = composeChangeSet([terraformChangeSetPart({ member: "root", plan: { format_version: "1.2", resource_changes: changes } })]);
|
|
146
|
+
const s = groupChangeSet(doc);
|
|
147
|
+
expect(s.unit).toBe("instance");
|
|
148
|
+
expect(s.groups.map((g) => [g.resource, g.units])).toEqual([
|
|
149
|
+
["aws_s3_bucket.logs", ['aws_s3_bucket.logs["a"]', 'aws_s3_bucket.logs["b"]', 'aws_s3_bucket.logs["c"]']],
|
|
150
|
+
["aws_s3_bucket.logs", ['aws_s3_bucket.logs["d"]']],
|
|
151
|
+
]);
|
|
152
|
+
expect(s.destroys).toEqual([{ member: "root", address: 'aws_s3_bucket.logs["d"]', type: "aws_s3_bucket", action: "replace" }]);
|
|
153
|
+
});
|
|
154
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -12,9 +12,12 @@ import { completions } from "./lsp/completions";
|
|
|
12
12
|
import { hover } from "./lsp/hover";
|
|
13
13
|
import { terraformConfigSchema, type TerraformConfig } from "./config";
|
|
14
14
|
import { renderTerraformRoots } from "./hcl/roots";
|
|
15
|
-
import {
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { auditRootName, LOCK_FILENAME, parseTerraformRootContent, RESOURCE_TYPE } from "./hcl/parse";
|
|
16
18
|
import { descendModules } from "./hcl/descend";
|
|
17
19
|
import { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
|
|
20
|
+
import { terraformCommands } from "./commands";
|
|
18
21
|
|
|
19
22
|
const loadSkills = createSkillsLoader(import.meta.url, [
|
|
20
23
|
{
|
|
@@ -187,6 +190,23 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
187
190
|
});
|
|
188
191
|
},
|
|
189
192
|
|
|
193
|
+
/** `chant terraform pin-rollout` (#3189). */
|
|
194
|
+
commands() {
|
|
195
|
+
return terraformCommands;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The `terraform.roots` a change touched (#3183): a changed file in the
|
|
200
|
+
* root's directory, in a local module it calls, or one of its var files.
|
|
201
|
+
* See `./changed-roots.ts`.
|
|
202
|
+
*/
|
|
203
|
+
async changedUnits(ctx) {
|
|
204
|
+
const roots = (ctx.config as { terraform?: TerraformConfig }).terraform?.roots ?? {};
|
|
205
|
+
if (Object.keys(roots).length === 0) return [];
|
|
206
|
+
const { changedRoots } = await import("./changed-roots");
|
|
207
|
+
return changedRoots(ctx.projectRoot, roots, ctx.changedFiles);
|
|
208
|
+
},
|
|
209
|
+
|
|
190
210
|
lintRules() {
|
|
191
211
|
return rules;
|
|
192
212
|
},
|
|
@@ -256,7 +276,10 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
256
276
|
async auditEntities(content: string, input?: AuditEntitiesInput): Promise<Map<string, Declarable>> {
|
|
257
277
|
try {
|
|
258
278
|
const root = auditRootName(input?.path);
|
|
259
|
-
|
|
279
|
+
// A local audit knows the root's directory, so it can say whether the
|
|
280
|
+
// lock file is beside the `.tf` files (TF040); a fetched one cannot.
|
|
281
|
+
const lockFile = input?.dir !== undefined ? existsSync(join(input.dir, LOCK_FILENAME)) : undefined;
|
|
282
|
+
const entities = await parseTerraformRootContent(content, root, undefined, lockFile === undefined ? undefined : { lockFile });
|
|
260
283
|
if (input?.dir === undefined) return entities;
|
|
261
284
|
const { entities: children } = await descendModules(entities, {
|
|
262
285
|
dir: input.dir,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affected Terragrunt units (#3415) against a real `terragrunt` 1.1+ and
|
|
3
|
+
* `git`: each case of `../__fixtures__/terragrunt/affected-cases.ts` is made
|
|
4
|
+
* as a commit on a copy of the `affected/` fixture. Terragrunt's own
|
|
5
|
+
* `[HEAD~1...HEAD]` filter must select exactly the case's `terragrunt`
|
|
6
|
+
* column (for the three supplements, nothing: the red half), and
|
|
7
|
+
* `findTerragruntAffected` must return the case's expected units and reasons.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is planned, so `tofu` is not needed. Skipped, with the reason in
|
|
10
|
+
* the title, when `terragrunt` or `git` is not on PATH.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
14
|
+
import { cpSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
19
|
+
import { AFFECTED_CASES } from "../__fixtures__/terragrunt/affected-cases";
|
|
20
|
+
import { defaultTerragruntExec, discoverTerragruntUnits, findTerragruntAffected, parseTerragruntFind, terragruntAffectedFindArgs } from "./index";
|
|
21
|
+
|
|
22
|
+
function onPath(cmd: string): boolean {
|
|
23
|
+
try {
|
|
24
|
+
execSync(`command -v ${cmd}`, { stdio: "ignore" });
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const skipReason = !onPath("terragrunt") ? "no terragrunt on PATH" : !onPath("git") ? "no git on PATH" : "";
|
|
32
|
+
const fixtures = join(dirname(fileURLToPath(import.meta.url)), "../__fixtures__/terragrunt");
|
|
33
|
+
const tmp = mkdtempSync(join(tmpdir(), "chant-tg-affected-accept-"));
|
|
34
|
+
afterAll(() => rmSync(tmp, { recursive: true, force: true }));
|
|
35
|
+
|
|
36
|
+
const gitEnv = { ...process.env, GIT_AUTHOR_NAME: "chant", GIT_AUTHOR_EMAIL: "chant@example.com", GIT_COMMITTER_NAME: "chant", GIT_COMMITTER_EMAIL: "chant@example.com" };
|
|
37
|
+
const git = (dir: string, ...args: string[]): string => execFileSync("git", args, { cwd: dir, env: gitEnv, encoding: "utf8" });
|
|
38
|
+
const generate = (dir: string): void => {
|
|
39
|
+
execFileSync("terragrunt", ["stack", "generate", "--no-color"], { cwd: dir, stdio: "ignore" });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A copy of the fixture as a git repository at its base commit, stacks generated. */
|
|
43
|
+
function baseRepo(name: string): string {
|
|
44
|
+
const dir = join(tmp, name.replace(/[^a-z0-9]+/gi, "-"));
|
|
45
|
+
cpSync(join(fixtures, "affected"), dir, { recursive: true });
|
|
46
|
+
renameSync(join(dir, "gitignore"), join(dir, ".gitignore"));
|
|
47
|
+
git(dir, "init", "-q", "-b", "main");
|
|
48
|
+
git(dir, "add", "-A");
|
|
49
|
+
git(dir, "commit", "-q", "-m", "base");
|
|
50
|
+
generate(dir);
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe.skipIf(skipReason !== "")(`affected Terragrunt units against a real terragrunt${skipReason ? ` (skipped: ${skipReason})` : ""}`, () => {
|
|
55
|
+
it("discovers what recorded/affected-find.json holds", async () => {
|
|
56
|
+
const dir = baseRepo("discovery");
|
|
57
|
+
const { units } = await discoverTerragruntUnits({ dir });
|
|
58
|
+
expect(units).toEqual(parseTerragruntFind(readFileSync(join(fixtures, "recorded/affected-find.json"), "utf8")));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const c of AFFECTED_CASES) {
|
|
62
|
+
it(
|
|
63
|
+
c.name,
|
|
64
|
+
async () => {
|
|
65
|
+
const dir = baseRepo(c.name);
|
|
66
|
+
c.change(dir);
|
|
67
|
+
git(dir, "add", "-A");
|
|
68
|
+
git(dir, "commit", "-q", "-m", c.name);
|
|
69
|
+
generate(dir);
|
|
70
|
+
expect(git(dir, "diff", "--name-only", "HEAD~1...HEAD").trim().split("\n").sort()).toEqual([...c.changed].sort());
|
|
71
|
+
|
|
72
|
+
const own = await defaultTerragruntExec("terragrunt", terragruntAffectedFindArgs({ base: "HEAD~1" }), { cwd: dir, env: {} });
|
|
73
|
+
expect(own.code, own.stderr).toBe(0);
|
|
74
|
+
expect(parseTerragruntFind(own.stdout).map((u) => u.path).sort()).toEqual(c.terragrunt);
|
|
75
|
+
|
|
76
|
+
const got = await findTerragruntAffected({ dir, base: "HEAD~1" });
|
|
77
|
+
expect(got.units).toEqual(c.expected);
|
|
78
|
+
expect(got.notes).toEqual([]);
|
|
79
|
+
},
|
|
80
|
+
60_000,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
});
|