@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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock outputs in Terragrunt waves (#3416), with Terragrunt stubbed. The
|
|
3
|
+
* render and output text is what Terragrunt 1.1.6 printed on the five-unit
|
|
4
|
+
* fixture; `terragrunt.acceptance.test.ts` runs the real binary.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
12
|
+
import { composeChangeSet, type ChangeSetPart } from "@intentius/chant/change-set";
|
|
13
|
+
import { ProvisionalWaveMemberError, waveSetDigest } from "@intentius/chant/gated-waves";
|
|
14
|
+
import { groupChangeSet, renderPlanSummaryText } from "@intentius/chant/plan-summary";
|
|
15
|
+
import {
|
|
16
|
+
applyTerragruntWave,
|
|
17
|
+
checkTerragruntWaveMocks,
|
|
18
|
+
describeMockRefusal,
|
|
19
|
+
failMockedParts,
|
|
20
|
+
markProvisional,
|
|
21
|
+
parseRenderedDependencies,
|
|
22
|
+
parseTerragruntOutputs,
|
|
23
|
+
planTerragruntWave,
|
|
24
|
+
PROVISIONAL_MARKER,
|
|
25
|
+
TerragruntMockRefusal,
|
|
26
|
+
terragruntMockReads,
|
|
27
|
+
terragruntMockWarnings,
|
|
28
|
+
terragruntWaveParts,
|
|
29
|
+
upstreamOfConfigPath,
|
|
30
|
+
parseTerragruntReport,
|
|
31
|
+
type RenderedDependency,
|
|
32
|
+
type TerragruntExec,
|
|
33
|
+
} from "./index";
|
|
34
|
+
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const fixture = join(here, "../__fixtures__/terragrunt/five-units");
|
|
37
|
+
const recorded = join(here, "../__fixtures__/terragrunt/recorded");
|
|
38
|
+
const read = (p: string): string => readFileSync(join(recorded, p), "utf8");
|
|
39
|
+
|
|
40
|
+
const tmps: string[] = [];
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
for (const t of tmps) rmSync(t, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
const tmp = (): string => {
|
|
45
|
+
const t = mkdtempSync(join(tmpdir(), "chant-tg-mocks-"));
|
|
46
|
+
tmps.push(t);
|
|
47
|
+
return t;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** `terragrunt render --json` in live/dev/app, as 1.1.6 printed it (trimmed to the parts read). */
|
|
51
|
+
const RENDER_DEV_APP = JSON.stringify({
|
|
52
|
+
dependencies: null,
|
|
53
|
+
dependency: {
|
|
54
|
+
live_dev_db: {
|
|
55
|
+
config_path: "../db",
|
|
56
|
+
enabled: null,
|
|
57
|
+
mock_outputs: { id: "mock" },
|
|
58
|
+
mock_outputs_allowed_terraform_commands: ["validate", "plan"],
|
|
59
|
+
mock_outputs_merge_strategy_with_state: null,
|
|
60
|
+
mock_outputs_merge_with_state: null,
|
|
61
|
+
name: "live_dev_db",
|
|
62
|
+
outputs: null,
|
|
63
|
+
skip: null,
|
|
64
|
+
},
|
|
65
|
+
live_dev_vpc: {
|
|
66
|
+
config_path: "../vpc",
|
|
67
|
+
enabled: null,
|
|
68
|
+
mock_outputs: { id: "mock" },
|
|
69
|
+
mock_outputs_allowed_terraform_commands: ["validate", "plan"],
|
|
70
|
+
mock_outputs_merge_strategy_with_state: null,
|
|
71
|
+
mock_outputs_merge_with_state: null,
|
|
72
|
+
name: "live_dev_vpc",
|
|
73
|
+
outputs: null,
|
|
74
|
+
skip: null,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
inputs: { name: "live-dev-app", upstream: ["mock", "mock"] },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** `output -json` from an applied vpc, and from one never applied. */
|
|
81
|
+
const OUTPUT_APPLIED = `{\n "id": {\n "sensitive": false,\n "type": "string",\n "value": "live-dev-vpc-id"\n }\n}\n`;
|
|
82
|
+
const OUTPUT_NONE = "{}\n";
|
|
83
|
+
|
|
84
|
+
const dep = (d: Partial<RenderedDependency> & { upstream: string }): RenderedDependency => ({ name: d.upstream.split("/").pop()!, mockOutputs: { id: "mock" }, mockAllowed: ["validate", "plan"], ...d });
|
|
85
|
+
|
|
86
|
+
describe("reading dependency blocks from terragrunt render", () => {
|
|
87
|
+
it("takes each block's upstream relative to the unit, its mocks, allow-list and merge strategy", () => {
|
|
88
|
+
expect(parseRenderedDependencies("live/dev/app", RENDER_DEV_APP, "/repo")).toEqual([
|
|
89
|
+
{ name: "live_dev_db", upstream: "live/dev/db", mockOutputs: { id: "mock" }, mockAllowed: ["validate", "plan"] },
|
|
90
|
+
{ name: "live_dev_vpc", upstream: "live/dev/vpc", mockOutputs: { id: "mock" }, mockAllowed: ["validate", "plan"] },
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("reads skip_outputs (printed as skip), enabled = false and both merge attributes", () => {
|
|
95
|
+
const doc = {
|
|
96
|
+
dependency: {
|
|
97
|
+
a: { name: "a", config_path: "/repo/live/a/terragrunt.hcl", skip: true, mock_outputs: { x: 1 } },
|
|
98
|
+
b: { name: "b", config_path: "../b", enabled: false },
|
|
99
|
+
c: { name: "c", config_path: "../c", mock_outputs_merge_with_state: true, mock_outputs: { x: 1 } },
|
|
100
|
+
d: { name: "d", config_path: "../d", mock_outputs_merge_strategy_with_state: "no_merge" },
|
|
101
|
+
e: { name: "e", config_path: "../e", mock_outputs_merge_strategy_with_state: "deep_map_only" },
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
expect(parseRenderedDependencies("live/u", doc, ["/link", "/repo"])).toEqual([
|
|
105
|
+
{ name: "a", upstream: "live/a", mockOutputs: { x: 1 }, skipOutputs: true },
|
|
106
|
+
{ name: "b", upstream: "live/b", disabled: true },
|
|
107
|
+
{ name: "c", upstream: "live/c", mockOutputs: { x: 1 }, mergeStrategy: "shallow" },
|
|
108
|
+
{ name: "d", upstream: "live/d" },
|
|
109
|
+
{ name: "e", upstream: "live/e", mergeStrategy: "deep_map_only" },
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("resolves config paths the way Terragrunt prints them", () => {
|
|
114
|
+
expect(upstreamOfConfigPath("live/prod/app", "../../dev/vpc/", "/r")).toBe("live/dev/vpc");
|
|
115
|
+
expect(upstreamOfConfigPath(".", "/private/var/r/live/vpc/terragrunt.hcl", ["/var/r", "/private/var/r"])).toBe("live/vpc");
|
|
116
|
+
expect(upstreamOfConfigPath(".", "/elsewhere/vpc", "/r")).toBe("/elsewhere/vpc");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("reads output -json with or without log lines before it", () => {
|
|
120
|
+
expect(parseTerragruntOutputs(OUTPUT_APPLIED)).toEqual({ id: "live-dev-vpc-id" });
|
|
121
|
+
expect(parseTerragruntOutputs(OUTPUT_NONE)).toEqual({});
|
|
122
|
+
expect(parseTerragruntOutputs("INFO tofu: init\n{}")).toEqual({});
|
|
123
|
+
expect(parseTerragruntOutputs("no json")).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("which dependencies would read mock_outputs", () => {
|
|
128
|
+
const reads = (deps: RenderedDependency[], outputs: Record<string, Record<string, unknown>>) =>
|
|
129
|
+
terragruntMockReads({ dependencies: new Map([["live/app", deps]]), outputs: new Map(Object.entries(outputs)) });
|
|
130
|
+
|
|
131
|
+
it("an upstream with no outputs, whether or not the block sets mocks", () => {
|
|
132
|
+
expect(reads([dep({ upstream: "live/vpc" })], { "live/vpc": {} })).toEqual([
|
|
133
|
+
{ unit: "live/app", dependency: "vpc", upstream: "live/vpc", reason: "no-outputs" },
|
|
134
|
+
]);
|
|
135
|
+
expect(reads([{ name: "vpc", upstream: "live/vpc" }], {})).toEqual([
|
|
136
|
+
{ unit: "live/app", dependency: "vpc", upstream: "live/vpc", reason: "no-outputs" },
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("nothing when the upstream has real outputs and no merge strategy", () => {
|
|
141
|
+
expect(reads([dep({ upstream: "live/vpc" })], { "live/vpc": { id: "real" } })).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("a partial mock: a merge strategy fills keys the real outputs lack", () => {
|
|
145
|
+
const d = dep({ upstream: "live/vpc", mockOutputs: { id: "m", subnets: { a: "m", b: "m" } }, mergeStrategy: "shallow" });
|
|
146
|
+
expect(reads([d], { "live/vpc": { id: "real" } })).toEqual([
|
|
147
|
+
{ unit: "live/app", dependency: "vpc", upstream: "live/vpc", reason: "partial", keys: ["subnets"] },
|
|
148
|
+
]);
|
|
149
|
+
expect(reads([d], { "live/vpc": { id: "real", subnets: { a: "real" } } })).toEqual([]);
|
|
150
|
+
expect(reads([{ ...d, mergeStrategy: "deep_map_only" }], { "live/vpc": { id: "real", subnets: { a: "real" } } })).toEqual([
|
|
151
|
+
{ unit: "live/app", dependency: "vpc", upstream: "live/vpc", reason: "partial", keys: ["subnets.b"] },
|
|
152
|
+
]);
|
|
153
|
+
// Mocks not allowed for plan: Terragrunt does not merge them at plan.
|
|
154
|
+
expect(reads([{ ...d, mockAllowed: ["validate"] }], { "live/vpc": { id: "real" } })).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("skip_outputs or enabled = false with mocks always reads the mock", () => {
|
|
158
|
+
expect(reads([dep({ upstream: "live/a", skipOutputs: true }), dep({ upstream: "live/b", disabled: true }), { name: "c", upstream: "live/c", skipOutputs: true }], {})).toEqual([
|
|
159
|
+
{ unit: "live/app", dependency: "a", upstream: "live/a", reason: "skip-outputs" },
|
|
160
|
+
{ unit: "live/app", dependency: "b", upstream: "live/b", reason: "disabled" },
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("the refusal names the upstream that must apply first", () => {
|
|
165
|
+
const text = describeMockRefusal(reads([dep({ upstream: "live/vpc" })], {}));
|
|
166
|
+
expect(text).toMatch(/not planned and is not gated/);
|
|
167
|
+
expect(text).toMatch(/Apply live\/vpc first/);
|
|
168
|
+
expect(text).toMatch(/live\/app \(dependency "vpc" on live\/vpc\): live\/vpc has no outputs yet/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("reads Terragrunt's mock warning from a plan log, relative to where it ran", () => {
|
|
172
|
+
const log =
|
|
173
|
+
"23:51:46.380 WARN [live/dev/db] Config /private/r/live/dev/vpc/terragrunt.hcl is a dependency of /private/r/live/dev/db/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.\n";
|
|
174
|
+
expect(terragruntMockWarnings(log + log, ["/r", "/private/r"])).toEqual([{ unit: "live/dev/db", upstream: "live/dev/vpc" }]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/** A planned wave-2 part set from the recorded fixture. */
|
|
179
|
+
function wave2Parts() {
|
|
180
|
+
const units = ["live/dev/db", "live/prod/app"];
|
|
181
|
+
const report = parseTerragruntReport(read("wave-2/plan-report.json"));
|
|
182
|
+
const planFor = (u: string): unknown => JSON.parse(read(`wave-2/json/${u}/tfplan.json`));
|
|
183
|
+
return terragruntWaveParts({ units, report, planFor, planner: "tofu" });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
describe("provisional plans", () => {
|
|
187
|
+
it("are left out of the change-set digest and refused by a wave's set digest", () => {
|
|
188
|
+
const real = wave2Parts();
|
|
189
|
+
const provisional = markProvisional(real);
|
|
190
|
+
expect(provisional.every((p) => p.member.provisional === true)).toBe(true);
|
|
191
|
+
const mixed = composeChangeSet([real[0]!, provisional[1]!]);
|
|
192
|
+
expect(mixed.digest).toBe(composeChangeSet([real[0]!]).digest);
|
|
193
|
+
expect(() => waveSetDigest(provisional.map((p) => ({ member: p.member.member, planDigest: p.member.planDigest!, provisional: p.member.provisional })))).toThrow(
|
|
194
|
+
ProvisionalWaveMemberError,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("never fold into a group of real plans, even with the same change", () => {
|
|
199
|
+
const real = wave2Parts()[0]!;
|
|
200
|
+
// The same unit's plan under a second name: the same change, by the summary's rules.
|
|
201
|
+
const twin = (member: string, provisional: boolean): ChangeSetPart => ({
|
|
202
|
+
member: { ...real.member, member, ...(provisional ? { provisional: true as const } : {}) },
|
|
203
|
+
entries: real.entries.map((e) => ({ ...e, member })),
|
|
204
|
+
});
|
|
205
|
+
const together = groupChangeSet(composeChangeSet([real, twin("copy/dev/db", false)]));
|
|
206
|
+
expect(together.groups.map((g) => g.units)).toEqual([["copy/dev/db", "live/dev/db"]]);
|
|
207
|
+
|
|
208
|
+
const summary = groupChangeSet(composeChangeSet([real, twin("copy/dev/db", true)]));
|
|
209
|
+
expect(summary.groups.map((g) => [g.units, g.provisional])).toEqual([
|
|
210
|
+
[["live/dev/db"], undefined],
|
|
211
|
+
[["copy/dev/db"], true],
|
|
212
|
+
]);
|
|
213
|
+
expect(summary.groups[1]!.id).not.toBe(summary.groups[0]!.id);
|
|
214
|
+
expect(summary.groups[1]!.extends).toBeUndefined();
|
|
215
|
+
expect(summary.groups.some((g) => g.outlier)).toBe(false);
|
|
216
|
+
expect(renderPlanSummaryText(summary)).toMatch(/\(provisional\)/);
|
|
217
|
+
expect(renderPlanSummaryText(summary).split("\n")[0]).toMatch(/1 provisional/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("a member the plan log shows reading a mock becomes a failed member", () => {
|
|
221
|
+
const parts = failMockedParts(wave2Parts(), [{ unit: "live/dev/db", upstream: "live/dev/vpc" }]);
|
|
222
|
+
expect(parts[0]!.member).toMatchObject({ status: "failed", planDigest: null });
|
|
223
|
+
expect(parts[0]!.member.error).toMatch(/planned on mock_outputs: live\/dev\/vpc/);
|
|
224
|
+
expect(parts[1]!.member.status).toBe("planned");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("a wave's plan with the mock check, Terragrunt stubbed", () => {
|
|
229
|
+
/** Answers render from the recorded text, output from `applied`, and records every call. */
|
|
230
|
+
const stub = (seen: string[][], applied: Set<string>, planLog = ""): TerragruntExec => async (_f, args) => {
|
|
231
|
+
seen.push([...args]);
|
|
232
|
+
if (args[0] === "render") {
|
|
233
|
+
const unit = args[args.indexOf("--working-dir") + 1]!;
|
|
234
|
+
return { code: 0, stdout: unit === "live/dev/app" ? RENDER_DEV_APP : '{"dependency":null}', stderr: "" };
|
|
235
|
+
}
|
|
236
|
+
if (args.includes("output")) {
|
|
237
|
+
const unit = args[args.indexOf("--working-dir") + 1]!;
|
|
238
|
+
return { code: 0, stdout: applied.has(unit) ? OUTPUT_APPLIED : OUTPUT_NONE, stderr: "" };
|
|
239
|
+
}
|
|
240
|
+
const at = (flag: string): string => args[args.indexOf(flag) + 1]!;
|
|
241
|
+
mkdirSync(dirname(at("--report-file")), { recursive: true });
|
|
242
|
+
writeFileSync(at("--report-file"), JSON.stringify([{ Name: "live/dev/app", Result: "succeeded" }]));
|
|
243
|
+
if (args.includes("plan")) {
|
|
244
|
+
mkdirSync(join(at("--json-out-dir"), "live/dev/app"), { recursive: true });
|
|
245
|
+
writeFileSync(join(at("--json-out-dir"), "live/dev/app/tfplan.json"), read("wave-2/json/live/dev/db/tfplan.json"));
|
|
246
|
+
mkdirSync(join(at("--out-dir"), "live/dev/app"), { recursive: true });
|
|
247
|
+
writeFileSync(join(at("--out-dir"), "live/dev/app/tfplan.tfplan"), "binary plan");
|
|
248
|
+
}
|
|
249
|
+
return { code: 0, stdout: "", stderr: planLog };
|
|
250
|
+
};
|
|
251
|
+
const repo = (): string => {
|
|
252
|
+
const dir = tmp();
|
|
253
|
+
cpSync(fixture, dir, { recursive: true });
|
|
254
|
+
return dir;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
it("refuses the wave before planning anything while an upstream has no outputs", async () => {
|
|
258
|
+
const dir = repo();
|
|
259
|
+
const seen: string[][] = [];
|
|
260
|
+
const err = await planTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub(seen, new Set(["live/dev/vpc"])) }).catch((e: unknown) => e);
|
|
261
|
+
expect(err).toBeInstanceOf(TerragruntMockRefusal);
|
|
262
|
+
expect((err as TerragruntMockRefusal).reads).toEqual([{ unit: "live/dev/app", dependency: "live_dev_db", upstream: "live/dev/db", reason: "no-outputs" }]);
|
|
263
|
+
expect((err as Error).message).toMatch(/Apply live\/dev\/db first/);
|
|
264
|
+
expect(seen.some((a) => a[0] === "run" && a.includes("--out-dir"))).toBe(false);
|
|
265
|
+
expect(existsSync(join(dir, "w", "plans"))).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("plans once every upstream has outputs", async () => {
|
|
269
|
+
const dir = repo();
|
|
270
|
+
const seen: string[][] = [];
|
|
271
|
+
const plan = await planTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub(seen, new Set(["live/dev/vpc", "live/dev/db"])) });
|
|
272
|
+
expect(plan.provisional).toBe(false);
|
|
273
|
+
expect(plan.parts.map((p) => [p.member.member, p.member.status, p.member.provisional])).toEqual([["live/dev/app", "planned", undefined]]);
|
|
274
|
+
expect(seen.filter((a) => a.includes("output")).map((a) => a[a.indexOf("--working-dir") + 1]).sort()).toEqual(["live/dev/db", "live/dev/vpc"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("checks the plan log after the fact", async () => {
|
|
278
|
+
const dir = repo();
|
|
279
|
+
const log = `WARN [live/dev/app] Config ${dir}/live/dev/db/terragrunt.hcl is a dependency of ${dir}/live/dev/app/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.`;
|
|
280
|
+
const plan = await planTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub([], new Set(["live/dev/vpc", "live/dev/db"]), log) });
|
|
281
|
+
expect(plan.parts[0]!.member).toMatchObject({ status: "failed", planDigest: null });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("a provisional plan skips the check, marks its members, and is never applied", async () => {
|
|
285
|
+
const dir = repo();
|
|
286
|
+
const seen: string[][] = [];
|
|
287
|
+
const plan = await planTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", provisional: true, exec: stub(seen, new Set()) });
|
|
288
|
+
expect(plan.provisional).toBe(true);
|
|
289
|
+
expect(seen.some((a) => a[0] === "render" || a.includes("output"))).toBe(false);
|
|
290
|
+
expect(plan.parts[0]!.member).toMatchObject({ status: "planned", provisional: true });
|
|
291
|
+
expect(existsSync(join(dir, "w", PROVISIONAL_MARKER))).toBe(true);
|
|
292
|
+
const before = seen.length;
|
|
293
|
+
await expect(applyTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub(seen, new Set()) })).rejects.toThrow(/provisional/);
|
|
294
|
+
expect(seen.length).toBe(before);
|
|
295
|
+
|
|
296
|
+
// Planned for real once the upstream applied, the marker is gone and the plan applies.
|
|
297
|
+
await planTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub(seen, new Set(["live/dev/vpc", "live/dev/db"])) });
|
|
298
|
+
expect(existsSync(join(dir, "w", PROVISIONAL_MARKER))).toBe(false);
|
|
299
|
+
const applied = await applyTerragruntWave({ dir, units: ["live/dev/app"], workDir: "w", exec: stub(seen, new Set()) });
|
|
300
|
+
expect(applied.results.map((r) => r.status)).toEqual(["succeeded"]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("refuses when an upstream's outputs cannot be read", async () => {
|
|
304
|
+
const dir = repo();
|
|
305
|
+
const exec: TerragruntExec = async (f, args, o) =>
|
|
306
|
+
args.includes("output") ? { code: 1, stdout: "", stderr: "Error: backend not reachable" } : stub([], new Set())(f, args, o);
|
|
307
|
+
await expect(checkTerragruntWaveMocks({ dir, units: ["live/dev/app"], exec })).rejects.toThrow(/could not read the outputs of live\/dev\/db.*backend not reachable/s);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock outputs in Terragrunt waves (#3416): which plans of a wave would read
|
|
3
|
+
* a dependency's `mock_outputs`, and the provisional mark a preview carries.
|
|
4
|
+
*
|
|
5
|
+
* A plan built on `mock_outputs` looks like a real one: Terragrunt says so in
|
|
6
|
+
* one log line only, and the plan JSON holds the mock values as if they were
|
|
7
|
+
* the upstream's (observed on 1.1.6). Gruntwork closed mocks in saved plans
|
|
8
|
+
* as won't-fix (gruntwork-io/terragrunt#2178). So a wave is checked before it
|
|
9
|
+
* is planned, and a wave that would read a mock is refused: it is not gated,
|
|
10
|
+
* and the refusal names the upstream that must apply first.
|
|
11
|
+
*
|
|
12
|
+
* The rules follow Terragrunt's own (`pkg/config/dependency.go` at v1.1.6,
|
|
13
|
+
* `getTerragruntOutputIfAppliedElseConfiguredDefault` and
|
|
14
|
+
* `shouldReturnMockOutputs`). For one `dependency` block, read at `plan`:
|
|
15
|
+
*
|
|
16
|
+
* - `enabled = false` or `skip_outputs = true`: Terragrunt never reads the
|
|
17
|
+
* upstream, so with `mock_outputs` set the unit always reads the mock.
|
|
18
|
+
* - The upstream has no outputs (`output -json` prints `{}`, as it does for
|
|
19
|
+
* a unit never applied): Terragrunt returns `mock_outputs` when they are
|
|
20
|
+
* set and allowed for `plan`, and fails otherwise. Either way the wave
|
|
21
|
+
* cannot plan on real values yet.
|
|
22
|
+
* - The upstream has outputs and the block sets a merge strategy
|
|
23
|
+
* (`mock_outputs_merge_strategy_with_state`, or the older
|
|
24
|
+
* `mock_outputs_merge_with_state = true`): every mock key the real outputs
|
|
25
|
+
* lack is filled from the mock. That is a partial mock, and it counts.
|
|
26
|
+
*
|
|
27
|
+
* The blocks come from `terragrunt render --json`, so includes, locals and
|
|
28
|
+
* expressions in `config_path` are Terragrunt's to evaluate, not chant's.
|
|
29
|
+
*
|
|
30
|
+
* A provisional plan is the other side: a dependent planned at PR time
|
|
31
|
+
* (`dependents: plan`), before its upstream applied. It may read mocks. Its
|
|
32
|
+
* change-set members carry `provisional: true`, which keeps it out of the
|
|
33
|
+
* change-set digest, out of any wave's set digest and out of every group of
|
|
34
|
+
* real plans, and its saved plans are never applied.
|
|
35
|
+
*
|
|
36
|
+
* Pure: nothing here spawns or reads files. `./run.ts` runs the commands.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
40
|
+
|
|
41
|
+
/** One `dependency` block as `terragrunt render --json` prints it. */
|
|
42
|
+
export interface RenderedDependency {
|
|
43
|
+
/** The block's label. */
|
|
44
|
+
name: string;
|
|
45
|
+
/** The upstream unit, relative to where Terragrunt runs, with `/` separators. */
|
|
46
|
+
upstream: string;
|
|
47
|
+
/** `mock_outputs`, when set. */
|
|
48
|
+
mockOutputs?: Record<string, unknown>;
|
|
49
|
+
/** `mock_outputs_allowed_terraform_commands`, when set. Unset or empty allows every command. */
|
|
50
|
+
mockAllowed?: string[];
|
|
51
|
+
/** `no_merge`, `shallow`, `deep_map_only` or `deep`, from either merge attribute. Unset is `no_merge`. */
|
|
52
|
+
mergeStrategy?: string;
|
|
53
|
+
/** `skip_outputs = true`. */
|
|
54
|
+
skipOutputs?: boolean;
|
|
55
|
+
/** `enabled = false`. */
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
60
|
+
|
|
61
|
+
/** `a/b/../c` to `a/c`, with `/` separators and no `./` or trailing `/`. An absolute path keeps its leading `/`. */
|
|
62
|
+
function normalize(path: string): string {
|
|
63
|
+
const p = path.replace(/\\/g, "/");
|
|
64
|
+
const out: string[] = [];
|
|
65
|
+
for (const seg of p.split("/")) {
|
|
66
|
+
if (seg === "" || seg === ".") continue;
|
|
67
|
+
if (seg === ".." && out.length > 0 && out[out.length - 1] !== "..") out.pop();
|
|
68
|
+
else out.push(seg);
|
|
69
|
+
}
|
|
70
|
+
const s = out.join("/");
|
|
71
|
+
return p.startsWith("/") ? `/${s}` : s || ".";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isAbsolute = (p: string): boolean => p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The upstream a `config_path` names, relative to where Terragrunt runs. A
|
|
78
|
+
* relative path is taken from the unit's directory. An absolute one is made
|
|
79
|
+
* relative to the first of `dirs` it sits under (a directory and its real
|
|
80
|
+
* path, when a symlink leads to it), and kept absolute when it sits under
|
|
81
|
+
* none. A path to the upstream's `terragrunt.hcl` names its directory.
|
|
82
|
+
*/
|
|
83
|
+
export function upstreamOfConfigPath(unit: string, configPath: string, dirs: string | readonly string[]): string {
|
|
84
|
+
const p = configPath.replace(/\\/g, "/").replace(/(^|\/)terragrunt\.hcl$/, "");
|
|
85
|
+
if (!isAbsolute(p)) return normalize(`${unit}/${p}`);
|
|
86
|
+
const abs = normalize(p);
|
|
87
|
+
for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
|
|
88
|
+
const root = normalize(dir);
|
|
89
|
+
if (abs === root) return ".";
|
|
90
|
+
if (abs.startsWith(`${root}/`)) return abs.slice(root.length + 1);
|
|
91
|
+
}
|
|
92
|
+
return abs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The `dependency` blocks of one unit, from `terragrunt render --json` run
|
|
97
|
+
* in it. `unit` is the unit's path relative to where Terragrunt runs (`dirs`).
|
|
98
|
+
* Throws when the output is not the rendered config.
|
|
99
|
+
*/
|
|
100
|
+
export function parseRenderedDependencies(unit: string, output: string | unknown, dirs: string | readonly string[]): RenderedDependency[] {
|
|
101
|
+
const doc = typeof output === "string" ? (JSON.parse(output) as unknown) : output;
|
|
102
|
+
if (!isObject(doc)) throw new Error("terragrunt render --json did not print an object");
|
|
103
|
+
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
104
|
+
const out: RenderedDependency[] = [];
|
|
105
|
+
for (const [label, raw] of Object.entries(blocks)) {
|
|
106
|
+
if (!isObject(raw) || typeof raw.config_path !== "string") continue;
|
|
107
|
+
const allowed = Array.isArray(raw.mock_outputs_allowed_terraform_commands)
|
|
108
|
+
? raw.mock_outputs_allowed_terraform_commands.filter((c): c is string => typeof c === "string")
|
|
109
|
+
: undefined;
|
|
110
|
+
const strategy =
|
|
111
|
+
typeof raw.mock_outputs_merge_strategy_with_state === "string"
|
|
112
|
+
? raw.mock_outputs_merge_strategy_with_state
|
|
113
|
+
: raw.mock_outputs_merge_with_state === true
|
|
114
|
+
? "shallow"
|
|
115
|
+
: undefined;
|
|
116
|
+
out.push({
|
|
117
|
+
name: typeof raw.name === "string" && raw.name ? raw.name : label,
|
|
118
|
+
upstream: upstreamOfConfigPath(unit, raw.config_path, dirs),
|
|
119
|
+
...(isObject(raw.mock_outputs) ? { mockOutputs: raw.mock_outputs } : {}),
|
|
120
|
+
...(allowed ? { mockAllowed: allowed } : {}),
|
|
121
|
+
...(strategy && strategy !== "no_merge" ? { mergeStrategy: strategy } : {}),
|
|
122
|
+
// render prints skip_outputs under `skip`.
|
|
123
|
+
...(raw.skip === true || raw.skip_outputs === true ? { skipOutputs: true } : {}),
|
|
124
|
+
...(raw.enabled === false ? { disabled: true } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The outputs `output -json` prints, by name, with their values. Undefined
|
|
132
|
+
* when the text holds no JSON object.
|
|
133
|
+
*/
|
|
134
|
+
export function parseTerragruntOutputs(output: string): Record<string, unknown> | undefined {
|
|
135
|
+
const text = output.trim();
|
|
136
|
+
const start = text.indexOf("{");
|
|
137
|
+
if (start < 0) return undefined;
|
|
138
|
+
let doc: unknown;
|
|
139
|
+
try {
|
|
140
|
+
doc = JSON.parse(text.slice(start));
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
if (!isObject(doc)) return undefined;
|
|
145
|
+
return Object.fromEntries(Object.entries(doc).map(([k, v]) => [k, isObject(v) && "value" in v ? v.value : v]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Arguments to `terragrunt` that print one unit's outputs as JSON. */
|
|
149
|
+
export function terragruntOutputArgs(unit: string): string[] {
|
|
150
|
+
return ["run", "--working-dir", unit, "--non-interactive", "--no-color", "--", "output", "-json"];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Arguments to `terragrunt` that print one unit's evaluated configuration as JSON. */
|
|
154
|
+
export function terragruntRenderArgs(unit: string): string[] {
|
|
155
|
+
return ["render", "--json", "--non-interactive", "--no-color", "--working-dir", unit];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Why one dependency of a wave's unit would read `mock_outputs`. */
|
|
159
|
+
export type TerragruntMockReason =
|
|
160
|
+
/** The upstream has no outputs: never applied, or applied with none. */
|
|
161
|
+
| "no-outputs"
|
|
162
|
+
/** A merge strategy fills keys the upstream's outputs lack from the mock. */
|
|
163
|
+
| "partial"
|
|
164
|
+
/** `skip_outputs = true` with `mock_outputs` set: the mock is always read. */
|
|
165
|
+
| "skip-outputs"
|
|
166
|
+
/** `enabled = false` with `mock_outputs` set: the mock is always read. */
|
|
167
|
+
| "disabled";
|
|
168
|
+
|
|
169
|
+
/** One dependency of a wave's unit that would plan on mock values. */
|
|
170
|
+
export interface TerragruntMockRead {
|
|
171
|
+
/** The wave's unit. */
|
|
172
|
+
unit: string;
|
|
173
|
+
/** The `dependency` block's label. */
|
|
174
|
+
dependency: string;
|
|
175
|
+
/** The upstream unit it names. */
|
|
176
|
+
upstream: string;
|
|
177
|
+
reason: TerragruntMockReason;
|
|
178
|
+
/** For `partial`: the mock keys the upstream's outputs lack, as dotted paths. */
|
|
179
|
+
keys?: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Mock paths the real outputs lack. `shallow` compares top-level keys; the deep strategies follow nested maps. */
|
|
183
|
+
function missingKeys(mock: Record<string, unknown>, real: Record<string, unknown>, deep: boolean, prefix = ""): string[] {
|
|
184
|
+
const out: string[] = [];
|
|
185
|
+
for (const [k, v] of Object.entries(mock)) {
|
|
186
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
187
|
+
if (!(k in real)) out.push(path);
|
|
188
|
+
else if (deep && isObject(v) && isObject(real[k])) out.push(...missingKeys(v, real[k] as Record<string, unknown>, deep, path));
|
|
189
|
+
}
|
|
190
|
+
return out.sort();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const allowsPlan = (d: RenderedDependency): boolean => !d.mockAllowed || d.mockAllowed.length === 0 || d.mockAllowed.includes("plan");
|
|
194
|
+
|
|
195
|
+
export interface TerragruntMockReadsInput {
|
|
196
|
+
/** Each wave unit's `dependency` blocks, by unit. */
|
|
197
|
+
dependencies: ReadonlyMap<string, readonly RenderedDependency[]>;
|
|
198
|
+
/**
|
|
199
|
+
* Each upstream's outputs, by unit path. `{}` for an upstream with none;
|
|
200
|
+
* a missing entry is read as none.
|
|
201
|
+
*/
|
|
202
|
+
outputs: ReadonlyMap<string, Record<string, unknown>>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Every dependency of the wave's units that would read `mock_outputs` at
|
|
207
|
+
* plan, sorted by unit and dependency. Empty when the wave plans on real
|
|
208
|
+
* outputs only.
|
|
209
|
+
*
|
|
210
|
+
* An upstream with no outputs is listed whether or not the block sets
|
|
211
|
+
* mocks: without them Terragrunt fails the plan, and with them it plans on
|
|
212
|
+
* fake values. Both mean the upstream must apply first.
|
|
213
|
+
*/
|
|
214
|
+
export function terragruntMockReads(input: TerragruntMockReadsInput): TerragruntMockRead[] {
|
|
215
|
+
const out: TerragruntMockRead[] = [];
|
|
216
|
+
for (const [unit, deps] of input.dependencies) {
|
|
217
|
+
for (const d of deps) {
|
|
218
|
+
const at = { unit, dependency: d.name, upstream: d.upstream };
|
|
219
|
+
if (d.disabled || d.skipOutputs) {
|
|
220
|
+
if (d.mockOutputs) out.push({ ...at, reason: d.disabled ? "disabled" : "skip-outputs" });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const real = input.outputs.get(d.upstream) ?? {};
|
|
224
|
+
if (Object.keys(real).length === 0) {
|
|
225
|
+
out.push({ ...at, reason: "no-outputs" });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (d.mockOutputs && d.mergeStrategy && allowsPlan(d)) {
|
|
229
|
+
const keys = missingKeys(d.mockOutputs, real, d.mergeStrategy !== "shallow");
|
|
230
|
+
if (keys.length > 0) out.push({ ...at, reason: "partial", keys });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.dependency < b.dependency ? -1 : a.dependency > b.dependency ? 1 : 0));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The upstreams a wave's `dependency` blocks read outputs from, sorted. */
|
|
238
|
+
export function terragruntOutputUpstreams(dependencies: ReadonlyMap<string, readonly RenderedDependency[]>): string[] {
|
|
239
|
+
const out = new Set<string>();
|
|
240
|
+
for (const deps of dependencies.values()) for (const d of deps) if (!d.disabled && !d.skipOutputs) out.add(d.upstream);
|
|
241
|
+
return [...out].sort();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readText(r: TerragruntMockRead): string {
|
|
245
|
+
const at = `${r.unit} (dependency "${r.dependency}" on ${r.upstream})`;
|
|
246
|
+
switch (r.reason) {
|
|
247
|
+
case "no-outputs":
|
|
248
|
+
return `${at}: ${r.upstream} has no outputs yet; apply ${r.upstream} first`;
|
|
249
|
+
case "partial":
|
|
250
|
+
return `${at}: ${r.upstream}'s outputs lack ${r.keys!.join(", ")}, which the merge strategy fills from mock_outputs; apply ${r.upstream} with those outputs first, or drop the merge strategy`;
|
|
251
|
+
case "skip-outputs":
|
|
252
|
+
return `${at}: skip_outputs = true with mock_outputs set always reads the mock; read the real outputs or drop mock_outputs`;
|
|
253
|
+
case "disabled":
|
|
254
|
+
return `${at}: enabled = false with mock_outputs set always reads the mock; enable the dependency or drop mock_outputs`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The refusal for a wave whose plans would read mocks: each unit, the upstream, and what must happen first. */
|
|
259
|
+
export function describeMockRefusal(reads: readonly TerragruntMockRead[]): string {
|
|
260
|
+
const upstreams = [...new Set(reads.filter((r) => r.reason === "no-outputs" || r.reason === "partial").map((r) => r.upstream))].sort();
|
|
261
|
+
const head =
|
|
262
|
+
`this wave was not planned and is not gated: its plans would read mock_outputs, so nobody could approve them as real. ` +
|
|
263
|
+
(upstreams.length > 0 ? `Apply ${upstreams.join(", ")} first, then plan the wave again.` : `Fix the dependency blocks below, then plan the wave again.`);
|
|
264
|
+
return [head, ...reads.map((r) => ` ${readText(r)}`)].join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The line Terragrunt logs when it hands a unit `mock_outputs` because the
|
|
269
|
+
* upstream has no outputs (`dependency.go` at v1.1.6): "Config <upstream> is
|
|
270
|
+
* a dependency of <unit> that has no outputs, but mock outputs provided and
|
|
271
|
+
* returning those in dependency output."
|
|
272
|
+
*/
|
|
273
|
+
const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no outputs, but mock outputs provided/g;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The units a plan log says read mocks, with the upstream each read,
|
|
277
|
+
* relative to where Terragrunt ran (`dirs`, as for
|
|
278
|
+
* {@link upstreamOfConfigPath}). This is the check after the fact: the check
|
|
279
|
+
* before planning ({@link terragruntMockReads}) is what refuses a wave, and
|
|
280
|
+
* this catches an upstream whose state went away in between.
|
|
281
|
+
*/
|
|
282
|
+
export function terragruntMockWarnings(log: string, dirs: string | readonly string[]): Array<{ unit: string; upstream: string }> {
|
|
283
|
+
const seen = new Set<string>();
|
|
284
|
+
const out: Array<{ unit: string; upstream: string }> = [];
|
|
285
|
+
for (const m of log.matchAll(MOCK_WARNING)) {
|
|
286
|
+
const unit = upstreamOfConfigPath(".", m[2]!, dirs);
|
|
287
|
+
const upstream = upstreamOfConfigPath(".", m[1]!, dirs);
|
|
288
|
+
const key = `${unit}\u0000${upstream}`;
|
|
289
|
+
if (seen.has(key)) continue;
|
|
290
|
+
seen.add(key);
|
|
291
|
+
out.push({ unit, upstream });
|
|
292
|
+
}
|
|
293
|
+
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.upstream < b.upstream ? -1 : 1));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The parts with each member whose unit the plan log says read mocks turned
|
|
298
|
+
* into a failed member, so a mock plan never reaches a gate.
|
|
299
|
+
*/
|
|
300
|
+
export function failMockedParts(parts: readonly ChangeSetPart[], mocked: ReadonlyArray<{ unit: string; upstream: string }>): ChangeSetPart[] {
|
|
301
|
+
const by = new Map<string, string[]>();
|
|
302
|
+
for (const m of mocked) by.set(m.unit, [...(by.get(m.unit) ?? []), m.upstream]);
|
|
303
|
+
return parts.map((p) => {
|
|
304
|
+
const ups = by.get(p.member.member);
|
|
305
|
+
if (!ups) return p;
|
|
306
|
+
return {
|
|
307
|
+
member: {
|
|
308
|
+
...p.member,
|
|
309
|
+
status: "failed" as const,
|
|
310
|
+
error: `planned on mock_outputs: ${ups.join(", ")} had no outputs when Terragrunt planned it; apply ${ups.join(", ")} first`,
|
|
311
|
+
planDigest: null,
|
|
312
|
+
},
|
|
313
|
+
entries: [],
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The parts of a provisional plan: each member marked, so no digest or group of real plans takes it. */
|
|
319
|
+
export function markProvisional(parts: readonly ChangeSetPart[]): ChangeSetPart[] {
|
|
320
|
+
return parts.map((p) => ({ ...p, member: { ...p.member, provisional: true as const } }));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The file a provisional plan leaves in its work directory, so its saved plans are never applied. */
|
|
324
|
+
export const PROVISIONAL_MARKER = "PROVISIONAL";
|