@intentius/chant-lexicon-terraform 0.124.38 → 0.124.39
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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/terragrunt/mocks.d.ts +16 -2
- package/dist/terragrunt/mocks.d.ts.map +1 -1
- package/dist/terragrunt/mocks.js +82 -14
- package/dist/terragrunt/run.d.ts +4 -1
- package/dist/terragrunt/run.d.ts.map +1 -1
- package/dist/terragrunt/run.js +29 -4
- package/package.json +2 -2
- package/src/terragrunt/mocks.test.ts +113 -0
- package/src/terragrunt/mocks.ts +88 -13
- package/src/terragrunt/run.ts +26 -2
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "034e3c2459bdc43fbf0aaec0a2ff992c3134d1cc340d6c314ea05449ea58a948",
|
|
5
5
|
"meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
|
|
6
6
|
"types/index.d.ts": "022927e6a7b351cc4cf52d5f973f60ccd140e5810e17b32fb8868965aa1db161",
|
|
7
7
|
"rules/plan-before-apply.ts": "7c10245b2c3335253b48ee7d8b075d89f43116bde20313a5d1031b343fef1282",
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"rules/tf045.ts": "760b1f4f385fbe7a86897efe3078fdeac11bcc9bfb806285d63db50488174b7b",
|
|
57
57
|
"skills/chant-terraform.md": "d9ca7b7c28aed0038ce775a3491150298492eed2858fd3651ed9bfca0e577ae8"
|
|
58
58
|
},
|
|
59
|
-
"composite": "
|
|
59
|
+
"composite": "8610a0946ccc539f566b883d405922a95fddd0a72ce81ba8c3c49ec155502cf0"
|
|
60
60
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* The blocks come from `terragrunt render --json`, so includes, locals and
|
|
28
28
|
* expressions in `config_path` are Terragrunt's to evaluate, not chant's.
|
|
29
|
+
* A block with an `expansion` (`for_each` or `count`, Terragrunt 1.2) is the
|
|
30
|
+
* exception: render prints the block's body as written, `${count.index}` and
|
|
31
|
+
* all (`pkg/config/config_as_cty.go:723-733` at v1.2.0; #4225). Its instances are the unit's `find --dependencies` paths that
|
|
32
|
+
* match that path with each `${...}` read as one path segment.
|
|
29
33
|
*
|
|
30
34
|
* A provisional plan is the other side: a dependent planned at PR time
|
|
31
35
|
* (`dependents: plan`), before its upstream applied. It may read mocks. Its
|
|
@@ -61,12 +65,22 @@ export interface RenderedDependency {
|
|
|
61
65
|
* none. A path to the upstream's `terragrunt.hcl` names its directory.
|
|
62
66
|
*/
|
|
63
67
|
export declare function upstreamOfConfigPath(unit: string, configPath: string, dirs: string | readonly string[]): string;
|
|
68
|
+
/** Whether `terragrunt render --json` output holds an expanded `dependency` block, which needs the unit's `find --dependencies` paths. */
|
|
69
|
+
export declare function hasExpandedDependency(output: string | unknown): boolean;
|
|
64
70
|
/**
|
|
65
71
|
* The `dependency` blocks of one unit, from `terragrunt render --json` run
|
|
66
72
|
* in it. `unit` is the unit's path relative to where Terragrunt runs (`dirs`).
|
|
67
|
-
*
|
|
73
|
+
*
|
|
74
|
+
* An expanded block (#4225) needs `found`, the unit's dependency paths from
|
|
75
|
+
* `terragrunt find --dependencies`, relative to `dirs`: it becomes one entry
|
|
76
|
+
* per found path that matches its `config_path` and that no plain block
|
|
77
|
+
* names, labelled `<label>[<upstream>]` and carrying the block's mocks,
|
|
78
|
+
* merge strategy and `skip_outputs`. `find` lists ordering-only
|
|
79
|
+
* `dependencies` edges too, which is why a path is matched, not assumed.
|
|
80
|
+
* Throws when the output is not the rendered config, when an expanded block
|
|
81
|
+
* comes without `found`, and when one matches no found path.
|
|
68
82
|
*/
|
|
69
|
-
export declare function parseRenderedDependencies(unit: string, output: string | unknown, dirs: string | readonly string[]): RenderedDependency[];
|
|
83
|
+
export declare function parseRenderedDependencies(unit: string, output: string | unknown, dirs: string | readonly string[], found?: readonly string[]): RenderedDependency[];
|
|
70
84
|
/**
|
|
71
85
|
* The outputs `output -json` prints, by name, with their values. Undefined
|
|
72
86
|
* when the text holds no JSON object.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mocks.d.ts","sourceRoot":"","sources":["../../src/terragrunt/mocks.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mocks.d.ts","sourceRoot":"","sources":["../../src/terragrunt/mocks.ts"],"names":[],"mappings":"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"}
|
package/dist/terragrunt/mocks.js
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* The blocks come from `terragrunt render --json`, so includes, locals and
|
|
28
28
|
* expressions in `config_path` are Terragrunt's to evaluate, not chant's.
|
|
29
|
+
* A block with an `expansion` (`for_each` or `count`, Terragrunt 1.2) is the
|
|
30
|
+
* exception: render prints the block's body as written, `${count.index}` and
|
|
31
|
+
* all (`pkg/config/config_as_cty.go:723-733` at v1.2.0; #4225). Its instances are the unit's `find --dependencies` paths that
|
|
32
|
+
* match that path with each `${...}` read as one path segment.
|
|
29
33
|
*
|
|
30
34
|
* A provisional plan is the other side: a dependent planned at PR time
|
|
31
35
|
* (`dependents: plan`), before its upstream applied. It may read mocks. Its
|
|
@@ -73,17 +77,58 @@ export function upstreamOfConfigPath(unit, configPath, dirs) {
|
|
|
73
77
|
}
|
|
74
78
|
return abs;
|
|
75
79
|
}
|
|
80
|
+
/** Whether a rendered `dependency` block is expanded (`for_each` or `count`), so its `config_path` is not a path yet. */
|
|
81
|
+
const isExpanded = (raw) => raw.expansion !== undefined ||
|
|
82
|
+
raw.for_each !== undefined ||
|
|
83
|
+
raw.count !== undefined ||
|
|
84
|
+
(typeof raw.config_path === "string" && raw.config_path.includes("${"));
|
|
85
|
+
function renderedDoc(output) {
|
|
86
|
+
const doc = typeof output === "string" ? JSON.parse(output) : output;
|
|
87
|
+
if (!isObject(doc))
|
|
88
|
+
throw new Error("terragrunt render --json did not print an object");
|
|
89
|
+
return doc;
|
|
90
|
+
}
|
|
91
|
+
/** Whether `terragrunt render --json` output holds an expanded `dependency` block, which needs the unit's `find --dependencies` paths. */
|
|
92
|
+
export function hasExpandedDependency(output) {
|
|
93
|
+
const doc = renderedDoc(output);
|
|
94
|
+
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
95
|
+
return Object.values(blocks).some((raw) => isObject(raw) && isExpanded(raw));
|
|
96
|
+
}
|
|
97
|
+
const SEGMENT = "\u0000";
|
|
98
|
+
/**
|
|
99
|
+
* The paths an expanded block's `config_path` can name, as a pattern over
|
|
100
|
+
* upstream paths: each `${...}` is one path segment, or part of one.
|
|
101
|
+
*/
|
|
102
|
+
function expandedPattern(unit, configPath, dirs) {
|
|
103
|
+
const marked = upstreamOfConfigPath(unit, configPath.replace(/\$\{[^}]*\}/g, SEGMENT), dirs);
|
|
104
|
+
const source = marked
|
|
105
|
+
.split(SEGMENT)
|
|
106
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
107
|
+
.join("[^/]+");
|
|
108
|
+
return new RegExp(`^${source}$`);
|
|
109
|
+
}
|
|
76
110
|
/**
|
|
77
111
|
* The `dependency` blocks of one unit, from `terragrunt render --json` run
|
|
78
112
|
* in it. `unit` is the unit's path relative to where Terragrunt runs (`dirs`).
|
|
79
|
-
*
|
|
113
|
+
*
|
|
114
|
+
* An expanded block (#4225) needs `found`, the unit's dependency paths from
|
|
115
|
+
* `terragrunt find --dependencies`, relative to `dirs`: it becomes one entry
|
|
116
|
+
* per found path that matches its `config_path` and that no plain block
|
|
117
|
+
* names, labelled `<label>[<upstream>]` and carrying the block's mocks,
|
|
118
|
+
* merge strategy and `skip_outputs`. `find` lists ordering-only
|
|
119
|
+
* `dependencies` edges too, which is why a path is matched, not assumed.
|
|
120
|
+
* Throws when the output is not the rendered config, when an expanded block
|
|
121
|
+
* comes without `found`, and when one matches no found path.
|
|
80
122
|
*/
|
|
81
|
-
export function parseRenderedDependencies(unit, output, dirs) {
|
|
82
|
-
const doc =
|
|
83
|
-
if (!isObject(doc))
|
|
84
|
-
throw new Error("terragrunt render --json did not print an object");
|
|
123
|
+
export function parseRenderedDependencies(unit, output, dirs, found) {
|
|
124
|
+
const doc = renderedDoc(output);
|
|
85
125
|
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
86
126
|
const out = [];
|
|
127
|
+
const plain = new Set();
|
|
128
|
+
for (const raw of Object.values(blocks)) {
|
|
129
|
+
if (isObject(raw) && typeof raw.config_path === "string" && !isExpanded(raw))
|
|
130
|
+
plain.add(upstreamOfConfigPath(unit, raw.config_path, dirs));
|
|
131
|
+
}
|
|
87
132
|
for (const [label, raw] of Object.entries(blocks)) {
|
|
88
133
|
if (!isObject(raw) || typeof raw.config_path !== "string")
|
|
89
134
|
continue;
|
|
@@ -95,16 +140,28 @@ export function parseRenderedDependencies(unit, output, dirs) {
|
|
|
95
140
|
: raw.mock_outputs_merge_with_state === true
|
|
96
141
|
? "shallow"
|
|
97
142
|
: undefined;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
upstream: upstreamOfConfigPath(unit, raw.config_path, dirs),
|
|
143
|
+
const name = typeof raw.name === "string" && raw.name ? raw.name : label;
|
|
144
|
+
const settings = {
|
|
101
145
|
...(isObject(raw.mock_outputs) ? { mockOutputs: raw.mock_outputs } : {}),
|
|
102
146
|
...(allowed ? { mockAllowed: allowed } : {}),
|
|
103
147
|
...(strategy && strategy !== "no_merge" ? { mergeStrategy: strategy } : {}),
|
|
104
148
|
// render prints skip_outputs under `skip`.
|
|
105
149
|
...(raw.skip === true || raw.skip_outputs === true ? { skipOutputs: true } : {}),
|
|
106
150
|
...(raw.enabled === false ? { disabled: true } : {}),
|
|
107
|
-
}
|
|
151
|
+
};
|
|
152
|
+
if (!isExpanded(raw)) {
|
|
153
|
+
out.push({ name, upstream: upstreamOfConfigPath(unit, raw.config_path, dirs), ...settings });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!found)
|
|
157
|
+
throw new Error(`dependency "${name}" is expanded (${raw.config_path}); its instances come from terragrunt find --dependencies`);
|
|
158
|
+
const pattern = expandedPattern(unit, raw.config_path, dirs);
|
|
159
|
+
const instances = [...new Set(found.map(normalize))].filter((p) => !plain.has(p) && pattern.test(p)).sort();
|
|
160
|
+
if (instances.length === 0) {
|
|
161
|
+
throw new Error(`dependency "${name}" is expanded (${raw.config_path}), and none of the unit's dependencies from terragrunt find matches it`);
|
|
162
|
+
}
|
|
163
|
+
for (const upstream of instances)
|
|
164
|
+
out.push({ name: `${name}[${upstream}]`, upstream, ...settings });
|
|
108
165
|
}
|
|
109
166
|
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
110
167
|
}
|
|
@@ -218,6 +275,13 @@ export function describeMockRefusal(reads) {
|
|
|
218
275
|
* returning those in dependency output."
|
|
219
276
|
*/
|
|
220
277
|
const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no outputs, but mock outputs provided/g;
|
|
278
|
+
/**
|
|
279
|
+
* The line Terragrunt 1.2 logs instead when it reads outputs straight from
|
|
280
|
+
* state and an upstream was never applied (`pkg/config/dependency.go:1166-1172`
|
|
281
|
+
* at v1.2.0): "Failed to read outputs from <upstream> referenced in <unit>
|
|
282
|
+
* as <name>, fallback to mock outputs. Error: ...".
|
|
283
|
+
*/
|
|
284
|
+
const MOCK_FALLBACK = /Failed to read outputs from (\S+?) referenced in (\S+?) as \S+?, fallback to mock outputs/g;
|
|
221
285
|
/**
|
|
222
286
|
* The units a plan log says read mocks, with the upstream each read,
|
|
223
287
|
* relative to where Terragrunt ran (`dirs`, as for
|
|
@@ -228,15 +292,19 @@ const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no output
|
|
|
228
292
|
export function terragruntMockWarnings(log, dirs) {
|
|
229
293
|
const seen = new Set();
|
|
230
294
|
const out = [];
|
|
231
|
-
|
|
232
|
-
const unit = upstreamOfConfigPath(".",
|
|
233
|
-
const upstream = upstreamOfConfigPath(".",
|
|
295
|
+
const add = (rawUnit, rawUpstream) => {
|
|
296
|
+
const unit = upstreamOfConfigPath(".", rawUnit, dirs);
|
|
297
|
+
const upstream = upstreamOfConfigPath(".", rawUpstream, dirs);
|
|
234
298
|
const key = `${unit}\u0000${upstream}`;
|
|
235
299
|
if (seen.has(key))
|
|
236
|
-
|
|
300
|
+
return;
|
|
237
301
|
seen.add(key);
|
|
238
302
|
out.push({ unit, upstream });
|
|
239
|
-
}
|
|
303
|
+
};
|
|
304
|
+
for (const m of log.matchAll(MOCK_WARNING))
|
|
305
|
+
add(m[2], m[1]);
|
|
306
|
+
for (const m of log.matchAll(MOCK_FALLBACK))
|
|
307
|
+
add(m[2], m[1]);
|
|
240
308
|
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.upstream < b.upstream ? -1 : 1));
|
|
241
309
|
}
|
|
242
310
|
/**
|
package/dist/terragrunt/run.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare class TerragruntMockRefusal extends TerragruntError {
|
|
|
77
77
|
readonly reads: TerragruntMockRead[];
|
|
78
78
|
constructor(reads: TerragruntMockRead[]);
|
|
79
79
|
}
|
|
80
|
+
/** Arguments to `terragrunt` that list one unit and the units it depends on, expanded blocks included. */
|
|
81
|
+
export declare function terragruntUnitDependenciesArgs(unit: string): string[];
|
|
80
82
|
export interface TerragruntMockCheckInput extends TerragruntRunOptions {
|
|
81
83
|
units: readonly string[];
|
|
82
84
|
/** How many `render` and `output` calls run at once. Default 8. */
|
|
@@ -85,7 +87,8 @@ export interface TerragruntMockCheckInput extends TerragruntRunOptions {
|
|
|
85
87
|
/**
|
|
86
88
|
* Which dependencies of the wave's units would read `mock_outputs` at plan,
|
|
87
89
|
* asked of Terragrunt: `render --json` in each unit for its evaluated
|
|
88
|
-
* `dependency` blocks
|
|
90
|
+
* `dependency` blocks (and `find --dependencies` for a unit with an expanded
|
|
91
|
+
* one, #4225), then `output -json` in each upstream they read. Empty
|
|
89
92
|
* when the wave would plan on real outputs only. Throws when Terragrunt
|
|
90
93
|
* cannot render a unit or read an upstream's outputs, since then nobody
|
|
91
94
|
* knows what the plan would read. A unit with no `terragrunt.hcl` in `dir`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/terragrunt/run.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEnF,OAAO,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/terragrunt/run.ts"],"names":[],"mappings":"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"}
|
package/dist/terragrunt/run.js
CHANGED
|
@@ -9,8 +9,8 @@ import { execFile } from "node:child_process";
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
import { plannerForBinary } from "../change-set.js";
|
|
12
|
-
import { parseTerragruntFind, terragruntFindArgs, terragruntVersionProblem, } from "./units.js";
|
|
13
|
-
import { describeMockRefusal, failMockedParts, markProvisional, parseRenderedDependencies, parseTerragruntOutputs, PROVISIONAL_MARKER, terragruntMockReads, terragruntMockWarnings, terragruntOutputArgs, terragruntOutputUpstreams, terragruntRenderArgs, } from "./mocks.js";
|
|
12
|
+
import { parseTerragruntFind, terragruntFindArgs, terragruntVersionProblem, unitPathFilter, } from "./units.js";
|
|
13
|
+
import { describeMockRefusal, failMockedParts, hasExpandedDependency, markProvisional, parseRenderedDependencies, parseTerragruntOutputs, PROVISIONAL_MARKER, terragruntMockReads, terragruntMockWarnings, terragruntOutputArgs, terragruntOutputUpstreams, terragruntRenderArgs, } from "./mocks.js";
|
|
14
14
|
import { parseTerragruntReport, terragruntEnv, terragruntUnitResults, terragruntWaveArgs, terragruntWaveParts, } from "./wave.js";
|
|
15
15
|
export const defaultTerragruntExec = (file, args, options) => new Promise((done) => {
|
|
16
16
|
execFile(file, [...args], { cwd: options.cwd, env: { ...process.env, ...options.env }, maxBuffer: 256 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
@@ -128,10 +128,15 @@ function dirsOf(dir) {
|
|
|
128
128
|
return [abs];
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/** Arguments to `terragrunt` that list one unit and the units it depends on, expanded blocks included. */
|
|
132
|
+
export function terragruntUnitDependenciesArgs(unit) {
|
|
133
|
+
return ["find", "--json", "--dependencies", "--no-color", "--filter", unitPathFilter(unit)];
|
|
134
|
+
}
|
|
131
135
|
/**
|
|
132
136
|
* Which dependencies of the wave's units would read `mock_outputs` at plan,
|
|
133
137
|
* asked of Terragrunt: `render --json` in each unit for its evaluated
|
|
134
|
-
* `dependency` blocks
|
|
138
|
+
* `dependency` blocks (and `find --dependencies` for a unit with an expanded
|
|
139
|
+
* one, #4225), then `output -json` in each upstream they read. Empty
|
|
135
140
|
* when the wave would plan on real outputs only. Throws when Terragrunt
|
|
136
141
|
* cannot render a unit or read an upstream's outputs, since then nobody
|
|
137
142
|
* knows what the plan would read. A unit with no `terragrunt.hcl` in `dir`
|
|
@@ -146,15 +151,35 @@ export async function checkTerragruntWaveMocks(input) {
|
|
|
146
151
|
// A unit this change removed has no terragrunt.hcl here: Terragrunt plans its destroy from the old checkout.
|
|
147
152
|
// It is not rendered; the plan log check still covers it.
|
|
148
153
|
const units = [...new Set(input.units)].sort().filter((u) => existsSync(join(resolve(input.dir), u, "terragrunt.hcl")));
|
|
154
|
+
const unitDependencies = async (unit) => {
|
|
155
|
+
const args = terragruntUnitDependenciesArgs(unit);
|
|
156
|
+
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
157
|
+
if (r.code !== 0)
|
|
158
|
+
throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}), so ${unit}'s expanded dependencies are unknown: ${r.stderr.trim()}`);
|
|
159
|
+
let found;
|
|
160
|
+
try {
|
|
161
|
+
found = parseTerragruntFind(r.stdout);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new TerragruntError(`could not read \`terragrunt find\` output for ${unit}: ${err instanceof Error ? err.message : String(err)}`);
|
|
165
|
+
}
|
|
166
|
+
return found.find((u) => u.path === unit)?.dependencies ?? [];
|
|
167
|
+
};
|
|
149
168
|
const rendered = await pool(units, limit, async (unit) => {
|
|
150
169
|
const args = terragruntRenderArgs(unit);
|
|
151
170
|
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
152
171
|
if (r.code !== 0)
|
|
153
172
|
throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}), so the wave's dependencies are unknown: ${r.stderr.trim()}`);
|
|
173
|
+
let found;
|
|
154
174
|
try {
|
|
155
|
-
|
|
175
|
+
// An expanded dependency (#4225) names its instances only through find.
|
|
176
|
+
if (hasExpandedDependency(r.stdout))
|
|
177
|
+
found = await unitDependencies(unit);
|
|
178
|
+
return [unit, parseRenderedDependencies(unit, r.stdout, dirs, found)];
|
|
156
179
|
}
|
|
157
180
|
catch (err) {
|
|
181
|
+
if (err instanceof TerragruntError)
|
|
182
|
+
throw err;
|
|
158
183
|
throw new TerragruntError(`could not read \`terragrunt render --json\` for ${unit}: ${err instanceof Error ? err.message : String(err)}`);
|
|
159
184
|
}
|
|
160
185
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-terraform",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"access": "public"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@intentius/chant": "^0.124.
|
|
70
|
+
"@intentius/chant": "^0.124.39",
|
|
71
71
|
"zod": "^4.3.6"
|
|
72
72
|
},
|
|
73
73
|
"description": "terraform lexicon for chant, existing Terraform root modules as declarable entities",
|
|
@@ -307,3 +307,116 @@ describe("a wave's plan with the mock check, Terragrunt stubbed", () => {
|
|
|
307
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
308
|
});
|
|
309
309
|
});
|
|
310
|
+
|
|
311
|
+
describe("expanded dependency blocks (Terragrunt 1.2, #4225)", () => {
|
|
312
|
+
/**
|
|
313
|
+
* `render --json` prints an expanded block's body as written
|
|
314
|
+
* (`pkg/config/config_as_cty.go:723-733` at v1.2.0), so `config_path`
|
|
315
|
+
* still holds `${count.index}` or `${each.value}`.
|
|
316
|
+
*/
|
|
317
|
+
const RENDER_EXPANDED = JSON.stringify({
|
|
318
|
+
dependency: {
|
|
319
|
+
shard: {
|
|
320
|
+
config_path: "../shard-${count.index}",
|
|
321
|
+
count: 2,
|
|
322
|
+
mock_outputs: { id: "mock" },
|
|
323
|
+
mock_outputs_allowed_terraform_commands: ["plan"],
|
|
324
|
+
mock_outputs_merge_strategy_with_state: "shallow",
|
|
325
|
+
name: "shard",
|
|
326
|
+
},
|
|
327
|
+
region: {
|
|
328
|
+
config_path: "../regions/${each.value}",
|
|
329
|
+
for_each: ["eu", "us"],
|
|
330
|
+
mock_outputs: { id: "mock" },
|
|
331
|
+
skip: true,
|
|
332
|
+
name: "region",
|
|
333
|
+
},
|
|
334
|
+
vpc: { config_path: "../vpc", mock_outputs: { id: "mock" }, name: "vpc" },
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
/** The unit's `find --dependencies` paths: the expanded instances, the plain upstream and an ordering-only `dependencies` edge. */
|
|
338
|
+
const FOUND = ["live/dev/shard-0", "live/dev/shard-1", "live/dev/regions/eu", "live/dev/regions/us", "live/dev/vpc", "live/dev/ordering"];
|
|
339
|
+
|
|
340
|
+
it("expands each block to the found paths its config_path matches, keeping its settings", () => {
|
|
341
|
+
const deps = parseRenderedDependencies("live/dev/app", RENDER_EXPANDED, "/r", FOUND);
|
|
342
|
+
expect(deps.map((d) => [d.name, d.upstream])).toEqual([
|
|
343
|
+
["region[live/dev/regions/eu]", "live/dev/regions/eu"],
|
|
344
|
+
["region[live/dev/regions/us]", "live/dev/regions/us"],
|
|
345
|
+
["shard[live/dev/shard-0]", "live/dev/shard-0"],
|
|
346
|
+
["shard[live/dev/shard-1]", "live/dev/shard-1"],
|
|
347
|
+
["vpc", "live/dev/vpc"],
|
|
348
|
+
]);
|
|
349
|
+
expect(deps.find((d) => d.upstream === "live/dev/shard-1")).toMatchObject({ mockOutputs: { id: "mock" }, mockAllowed: ["plan"], mergeStrategy: "shallow" });
|
|
350
|
+
expect(deps.find((d) => d.upstream === "live/dev/regions/us")).toMatchObject({ skipOutputs: true });
|
|
351
|
+
// The ordering-only edge is not a data dependency.
|
|
352
|
+
expect(deps.some((d) => d.upstream === "live/dev/ordering")).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("never takes the literal config_path as an upstream, and needs find to expand", () => {
|
|
356
|
+
expect(() => parseRenderedDependencies("live/dev/app", RENDER_EXPANDED, "/r")).toThrow(/expanded .*terragrunt find --dependencies/);
|
|
357
|
+
expect(() => parseRenderedDependencies("live/dev/app", RENDER_EXPANDED, "/r", ["live/dev/vpc"])).toThrow(/none of the unit's dependencies from terragrunt find matches it/);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("a plain block's upstream is not claimed by an expanded one", () => {
|
|
361
|
+
const render = JSON.stringify({
|
|
362
|
+
dependency: {
|
|
363
|
+
any: { config_path: "../${each.value}", for_each: ["a", "b"], name: "any" },
|
|
364
|
+
a: { config_path: "../a", name: "a" },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const deps = parseRenderedDependencies("live/app", render, "/r", ["live/a", "live/b"]);
|
|
368
|
+
expect(deps.map((d) => [d.name, d.upstream])).toEqual([
|
|
369
|
+
["a", "live/a"],
|
|
370
|
+
["any[live/b]", "live/b"],
|
|
371
|
+
]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("the wave check reads each expanded upstream's outputs, through find for that unit only", async () => {
|
|
375
|
+
const dir = tmp();
|
|
376
|
+
cpSync(fixture, dir, { recursive: true });
|
|
377
|
+
const seen: string[][] = [];
|
|
378
|
+
const exec: TerragruntExec = async (_f, args) => {
|
|
379
|
+
seen.push([...args]);
|
|
380
|
+
if (args[0] === "render") return { code: 0, stdout: RENDER_EXPANDED, stderr: "" };
|
|
381
|
+
if (args[0] === "find") {
|
|
382
|
+
const doc = [{ type: "unit", path: "live/dev/app", dependencies: FOUND.map((p) => `./${p}`) }];
|
|
383
|
+
return { code: 0, stdout: JSON.stringify(doc), stderr: "" };
|
|
384
|
+
}
|
|
385
|
+
const unit = args[args.indexOf("--working-dir") + 1]!;
|
|
386
|
+
return { code: 0, stdout: unit === "live/dev/shard-1" ? OUTPUT_NONE : OUTPUT_APPLIED, stderr: "" };
|
|
387
|
+
};
|
|
388
|
+
const reads = await checkTerragruntWaveMocks({ dir, units: ["live/dev/app"], exec });
|
|
389
|
+
expect(reads).toEqual([
|
|
390
|
+
{ unit: "live/dev/app", dependency: "region[live/dev/regions/eu]", upstream: "live/dev/regions/eu", reason: "skip-outputs" },
|
|
391
|
+
{ unit: "live/dev/app", dependency: "region[live/dev/regions/us]", upstream: "live/dev/regions/us", reason: "skip-outputs" },
|
|
392
|
+
{ unit: "live/dev/app", dependency: "shard[live/dev/shard-1]", upstream: "live/dev/shard-1", reason: "no-outputs" },
|
|
393
|
+
]);
|
|
394
|
+
expect(seen.find((a) => a[0] === "find")).toEqual(["find", "--json", "--dependencies", "--no-color", "--filter", "{./live/dev/app}"]);
|
|
395
|
+
const outputsRead = seen.filter((a) => a.includes("output")).map((a) => a[a.indexOf("--working-dir") + 1]).sort();
|
|
396
|
+
expect(outputsRead).toEqual(["live/dev/shard-0", "live/dev/shard-1", "live/dev/vpc"]);
|
|
397
|
+
expect(outputsRead.some((u) => u.includes("${"))).toBe(false);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("a unit with plain blocks only never runs find", async () => {
|
|
401
|
+
const dir = tmp();
|
|
402
|
+
cpSync(fixture, dir, { recursive: true });
|
|
403
|
+
const seen: string[][] = [];
|
|
404
|
+
const exec: TerragruntExec = async (_f, args) => {
|
|
405
|
+
seen.push([...args]);
|
|
406
|
+
if (args[0] === "render") return { code: 0, stdout: RENDER_DEV_APP, stderr: "" };
|
|
407
|
+
return { code: 0, stdout: OUTPUT_APPLIED, stderr: "" };
|
|
408
|
+
};
|
|
409
|
+
await checkTerragruntWaveMocks({ dir, units: ["live/dev/app"], exec });
|
|
410
|
+
expect(seen.some((a) => a[0] === "find")).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("reads Terragrunt 1.2's fallback warning beside the 1.1 one", () => {
|
|
414
|
+
const log =
|
|
415
|
+
"WARN [live/dev/app] Failed to read outputs from /private/r/live/dev/shard-1/terragrunt.hcl referenced in /private/r/live/dev/app/terragrunt.hcl as shard, fallback to mock outputs. Error: no state\n" +
|
|
416
|
+
"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";
|
|
417
|
+
expect(terragruntMockWarnings(log + log, ["/r", "/private/r"])).toEqual([
|
|
418
|
+
{ unit: "live/dev/app", upstream: "live/dev/shard-1" },
|
|
419
|
+
{ unit: "live/dev/db", upstream: "live/dev/vpc" },
|
|
420
|
+
]);
|
|
421
|
+
});
|
|
422
|
+
});
|
package/src/terragrunt/mocks.ts
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* The blocks come from `terragrunt render --json`, so includes, locals and
|
|
28
28
|
* expressions in `config_path` are Terragrunt's to evaluate, not chant's.
|
|
29
|
+
* A block with an `expansion` (`for_each` or `count`, Terragrunt 1.2) is the
|
|
30
|
+
* exception: render prints the block's body as written, `${count.index}` and
|
|
31
|
+
* all (`pkg/config/config_as_cty.go:723-733` at v1.2.0; #4225). Its instances are the unit's `find --dependencies` paths that
|
|
32
|
+
* match that path with each `${...}` read as one path segment.
|
|
29
33
|
*
|
|
30
34
|
* A provisional plan is the other side: a dependent planned at PR time
|
|
31
35
|
* (`dependents: plan`), before its upstream applied. It may read mocks. Its
|
|
@@ -92,16 +96,67 @@ export function upstreamOfConfigPath(unit: string, configPath: string, dirs: str
|
|
|
92
96
|
return abs;
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/** Whether a rendered `dependency` block is expanded (`for_each` or `count`), so its `config_path` is not a path yet. */
|
|
100
|
+
const isExpanded = (raw: Record<string, unknown>): boolean =>
|
|
101
|
+
raw.expansion !== undefined ||
|
|
102
|
+
raw.for_each !== undefined ||
|
|
103
|
+
raw.count !== undefined ||
|
|
104
|
+
(typeof raw.config_path === "string" && raw.config_path.includes("${"));
|
|
105
|
+
|
|
106
|
+
function renderedDoc(output: string | unknown): Record<string, unknown> {
|
|
107
|
+
const doc = typeof output === "string" ? (JSON.parse(output) as unknown) : output;
|
|
108
|
+
if (!isObject(doc)) throw new Error("terragrunt render --json did not print an object");
|
|
109
|
+
return doc;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether `terragrunt render --json` output holds an expanded `dependency` block, which needs the unit's `find --dependencies` paths. */
|
|
113
|
+
export function hasExpandedDependency(output: string | unknown): boolean {
|
|
114
|
+
const doc = renderedDoc(output);
|
|
115
|
+
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
116
|
+
return Object.values(blocks).some((raw) => isObject(raw) && isExpanded(raw));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const SEGMENT = "\u0000";
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The paths an expanded block's `config_path` can name, as a pattern over
|
|
123
|
+
* upstream paths: each `${...}` is one path segment, or part of one.
|
|
124
|
+
*/
|
|
125
|
+
function expandedPattern(unit: string, configPath: string, dirs: string | readonly string[]): RegExp {
|
|
126
|
+
const marked = upstreamOfConfigPath(unit, configPath.replace(/\$\{[^}]*\}/g, SEGMENT), dirs);
|
|
127
|
+
const source = marked
|
|
128
|
+
.split(SEGMENT)
|
|
129
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
130
|
+
.join("[^/]+");
|
|
131
|
+
return new RegExp(`^${source}$`);
|
|
132
|
+
}
|
|
133
|
+
|
|
95
134
|
/**
|
|
96
135
|
* The `dependency` blocks of one unit, from `terragrunt render --json` run
|
|
97
136
|
* in it. `unit` is the unit's path relative to where Terragrunt runs (`dirs`).
|
|
98
|
-
*
|
|
137
|
+
*
|
|
138
|
+
* An expanded block (#4225) needs `found`, the unit's dependency paths from
|
|
139
|
+
* `terragrunt find --dependencies`, relative to `dirs`: it becomes one entry
|
|
140
|
+
* per found path that matches its `config_path` and that no plain block
|
|
141
|
+
* names, labelled `<label>[<upstream>]` and carrying the block's mocks,
|
|
142
|
+
* merge strategy and `skip_outputs`. `find` lists ordering-only
|
|
143
|
+
* `dependencies` edges too, which is why a path is matched, not assumed.
|
|
144
|
+
* Throws when the output is not the rendered config, when an expanded block
|
|
145
|
+
* comes without `found`, and when one matches no found path.
|
|
99
146
|
*/
|
|
100
|
-
export function parseRenderedDependencies(
|
|
101
|
-
|
|
102
|
-
|
|
147
|
+
export function parseRenderedDependencies(
|
|
148
|
+
unit: string,
|
|
149
|
+
output: string | unknown,
|
|
150
|
+
dirs: string | readonly string[],
|
|
151
|
+
found?: readonly string[],
|
|
152
|
+
): RenderedDependency[] {
|
|
153
|
+
const doc = renderedDoc(output);
|
|
103
154
|
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
104
155
|
const out: RenderedDependency[] = [];
|
|
156
|
+
const plain = new Set<string>();
|
|
157
|
+
for (const raw of Object.values(blocks)) {
|
|
158
|
+
if (isObject(raw) && typeof raw.config_path === "string" && !isExpanded(raw)) plain.add(upstreamOfConfigPath(unit, raw.config_path, dirs));
|
|
159
|
+
}
|
|
105
160
|
for (const [label, raw] of Object.entries(blocks)) {
|
|
106
161
|
if (!isObject(raw) || typeof raw.config_path !== "string") continue;
|
|
107
162
|
const allowed = Array.isArray(raw.mock_outputs_allowed_terraform_commands)
|
|
@@ -113,16 +168,26 @@ export function parseRenderedDependencies(unit: string, output: string | unknown
|
|
|
113
168
|
: raw.mock_outputs_merge_with_state === true
|
|
114
169
|
? "shallow"
|
|
115
170
|
: undefined;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
upstream: upstreamOfConfigPath(unit, raw.config_path, dirs),
|
|
171
|
+
const name = typeof raw.name === "string" && raw.name ? raw.name : label;
|
|
172
|
+
const settings = {
|
|
119
173
|
...(isObject(raw.mock_outputs) ? { mockOutputs: raw.mock_outputs } : {}),
|
|
120
174
|
...(allowed ? { mockAllowed: allowed } : {}),
|
|
121
175
|
...(strategy && strategy !== "no_merge" ? { mergeStrategy: strategy } : {}),
|
|
122
176
|
// render prints skip_outputs under `skip`.
|
|
123
177
|
...(raw.skip === true || raw.skip_outputs === true ? { skipOutputs: true } : {}),
|
|
124
178
|
...(raw.enabled === false ? { disabled: true } : {}),
|
|
125
|
-
}
|
|
179
|
+
};
|
|
180
|
+
if (!isExpanded(raw)) {
|
|
181
|
+
out.push({ name, upstream: upstreamOfConfigPath(unit, raw.config_path, dirs), ...settings });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!found) throw new Error(`dependency "${name}" is expanded (${raw.config_path}); its instances come from terragrunt find --dependencies`);
|
|
185
|
+
const pattern = expandedPattern(unit, raw.config_path, dirs);
|
|
186
|
+
const instances = [...new Set(found.map(normalize))].filter((p) => !plain.has(p) && pattern.test(p)).sort();
|
|
187
|
+
if (instances.length === 0) {
|
|
188
|
+
throw new Error(`dependency "${name}" is expanded (${raw.config_path}), and none of the unit's dependencies from terragrunt find matches it`);
|
|
189
|
+
}
|
|
190
|
+
for (const upstream of instances) out.push({ name: `${name}[${upstream}]`, upstream, ...settings });
|
|
126
191
|
}
|
|
127
192
|
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
128
193
|
}
|
|
@@ -272,6 +337,14 @@ export function describeMockRefusal(reads: readonly TerragruntMockRead[]): strin
|
|
|
272
337
|
*/
|
|
273
338
|
const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no outputs, but mock outputs provided/g;
|
|
274
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The line Terragrunt 1.2 logs instead when it reads outputs straight from
|
|
342
|
+
* state and an upstream was never applied (`pkg/config/dependency.go:1166-1172`
|
|
343
|
+
* at v1.2.0): "Failed to read outputs from <upstream> referenced in <unit>
|
|
344
|
+
* as <name>, fallback to mock outputs. Error: ...".
|
|
345
|
+
*/
|
|
346
|
+
const MOCK_FALLBACK = /Failed to read outputs from (\S+?) referenced in (\S+?) as \S+?, fallback to mock outputs/g;
|
|
347
|
+
|
|
275
348
|
/**
|
|
276
349
|
* The units a plan log says read mocks, with the upstream each read,
|
|
277
350
|
* relative to where Terragrunt ran (`dirs`, as for
|
|
@@ -282,14 +355,16 @@ const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no output
|
|
|
282
355
|
export function terragruntMockWarnings(log: string, dirs: string | readonly string[]): Array<{ unit: string; upstream: string }> {
|
|
283
356
|
const seen = new Set<string>();
|
|
284
357
|
const out: Array<{ unit: string; upstream: string }> = [];
|
|
285
|
-
|
|
286
|
-
const unit = upstreamOfConfigPath(".",
|
|
287
|
-
const upstream = upstreamOfConfigPath(".",
|
|
358
|
+
const add = (rawUnit: string, rawUpstream: string): void => {
|
|
359
|
+
const unit = upstreamOfConfigPath(".", rawUnit, dirs);
|
|
360
|
+
const upstream = upstreamOfConfigPath(".", rawUpstream, dirs);
|
|
288
361
|
const key = `${unit}\u0000${upstream}`;
|
|
289
|
-
if (seen.has(key))
|
|
362
|
+
if (seen.has(key)) return;
|
|
290
363
|
seen.add(key);
|
|
291
364
|
out.push({ unit, upstream });
|
|
292
|
-
}
|
|
365
|
+
};
|
|
366
|
+
for (const m of log.matchAll(MOCK_WARNING)) add(m[2]!, m[1]!);
|
|
367
|
+
for (const m of log.matchAll(MOCK_FALLBACK)) add(m[2]!, m[1]!);
|
|
293
368
|
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.upstream < b.upstream ? -1 : 1));
|
|
294
369
|
}
|
|
295
370
|
|
package/src/terragrunt/run.ts
CHANGED
|
@@ -15,12 +15,14 @@ import {
|
|
|
15
15
|
parseTerragruntFind,
|
|
16
16
|
terragruntFindArgs,
|
|
17
17
|
terragruntVersionProblem,
|
|
18
|
+
unitPathFilter,
|
|
18
19
|
type TerragruntFindOptions,
|
|
19
20
|
type TerragruntUnit,
|
|
20
21
|
} from "./units";
|
|
21
22
|
import {
|
|
22
23
|
describeMockRefusal,
|
|
23
24
|
failMockedParts,
|
|
25
|
+
hasExpandedDependency,
|
|
24
26
|
markProvisional,
|
|
25
27
|
parseRenderedDependencies,
|
|
26
28
|
parseTerragruntOutputs,
|
|
@@ -208,6 +210,11 @@ function dirsOf(dir: string): string[] {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
/** Arguments to `terragrunt` that list one unit and the units it depends on, expanded blocks included. */
|
|
214
|
+
export function terragruntUnitDependenciesArgs(unit: string): string[] {
|
|
215
|
+
return ["find", "--json", "--dependencies", "--no-color", "--filter", unitPathFilter(unit)];
|
|
216
|
+
}
|
|
217
|
+
|
|
211
218
|
export interface TerragruntMockCheckInput extends TerragruntRunOptions {
|
|
212
219
|
units: readonly string[];
|
|
213
220
|
/** How many `render` and `output` calls run at once. Default 8. */
|
|
@@ -217,7 +224,8 @@ export interface TerragruntMockCheckInput extends TerragruntRunOptions {
|
|
|
217
224
|
/**
|
|
218
225
|
* Which dependencies of the wave's units would read `mock_outputs` at plan,
|
|
219
226
|
* asked of Terragrunt: `render --json` in each unit for its evaluated
|
|
220
|
-
* `dependency` blocks
|
|
227
|
+
* `dependency` blocks (and `find --dependencies` for a unit with an expanded
|
|
228
|
+
* one, #4225), then `output -json` in each upstream they read. Empty
|
|
221
229
|
* when the wave would plan on real outputs only. Throws when Terragrunt
|
|
222
230
|
* cannot render a unit or read an upstream's outputs, since then nobody
|
|
223
231
|
* knows what the plan would read. A unit with no `terragrunt.hcl` in `dir`
|
|
@@ -232,13 +240,29 @@ export async function checkTerragruntWaveMocks(input: TerragruntMockCheckInput):
|
|
|
232
240
|
// A unit this change removed has no terragrunt.hcl here: Terragrunt plans its destroy from the old checkout.
|
|
233
241
|
// It is not rendered; the plan log check still covers it.
|
|
234
242
|
const units = [...new Set(input.units)].sort().filter((u) => existsSync(join(resolve(input.dir), u, "terragrunt.hcl")));
|
|
243
|
+
const unitDependencies = async (unit: string): Promise<string[]> => {
|
|
244
|
+
const args = terragruntUnitDependenciesArgs(unit);
|
|
245
|
+
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
246
|
+
if (r.code !== 0) throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}), so ${unit}'s expanded dependencies are unknown: ${r.stderr.trim()}`);
|
|
247
|
+
let found: TerragruntUnit[];
|
|
248
|
+
try {
|
|
249
|
+
found = parseTerragruntFind(r.stdout);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
throw new TerragruntError(`could not read \`terragrunt find\` output for ${unit}: ${err instanceof Error ? err.message : String(err)}`);
|
|
252
|
+
}
|
|
253
|
+
return found.find((u) => u.path === unit)?.dependencies ?? [];
|
|
254
|
+
};
|
|
235
255
|
const rendered = await pool(units, limit, async (unit): Promise<[string, RenderedDependency[]]> => {
|
|
236
256
|
const args = terragruntRenderArgs(unit);
|
|
237
257
|
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
238
258
|
if (r.code !== 0) throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}), so the wave's dependencies are unknown: ${r.stderr.trim()}`);
|
|
259
|
+
let found: string[] | undefined;
|
|
239
260
|
try {
|
|
240
|
-
|
|
261
|
+
// An expanded dependency (#4225) names its instances only through find.
|
|
262
|
+
if (hasExpandedDependency(r.stdout)) found = await unitDependencies(unit);
|
|
263
|
+
return [unit, parseRenderedDependencies(unit, r.stdout, dirs, found)];
|
|
241
264
|
} catch (err) {
|
|
265
|
+
if (err instanceof TerragruntError) throw err;
|
|
242
266
|
throw new TerragruntError(`could not read \`terragrunt render --json\` for ${unit}: ${err instanceof Error ? err.message : String(err)}`);
|
|
243
267
|
}
|
|
244
268
|
});
|