@intentius/chant-lexicon-terraform 0.124.38 → 0.124.40

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "11535ffc33281743a4e1d0e6899c8b70b15c23e5c7e03b5f33d2e4b79bfe55e9",
4
+ "manifest.json": "f0598da0da29cbbc7d21dace65620dc744cab6b4ef829469235b108ce0b65bc5",
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": "70fa23ca43d5d767899be5780e59efe176f954c2da8676f2e0a17ff841e5e272"
59
+ "composite": "b0456d4d4bf0a1271966cebbbd564f7e0b081b25cdaed84abc62225374258d22"
60
60
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terraform",
3
- "version": "0.124.38",
3
+ "version": "0.124.40",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Terraform"
6
6
  }
@@ -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
- * Throws when the output is not the rendered config.
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":"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"}
1
+ {"version":3,"file":"mocks.d.ts","sourceRoot":"","sources":["../../src/terragrunt/mocks.ts"],"names":[],"mappings":"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"}
@@ -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
- * Throws when the output is not the rendered config.
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 = typeof output === "string" ? JSON.parse(output) : output;
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
- out.push({
99
- name: typeof raw.name === "string" && raw.name ? raw.name : label,
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
- for (const m of log.matchAll(MOCK_WARNING)) {
232
- const unit = upstreamOfConfigPath(".", m[2], dirs);
233
- const upstream = upstreamOfConfigPath(".", m[1], dirs);
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
- continue;
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
  /**
@@ -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, then `output -json` in each upstream they read. Empty
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":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/terragrunt/run.ts"],"names":[],"mappings":"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"}
@@ -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, then `output -json` in each upstream they read. Empty
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
- return [unit, parseRenderedDependencies(unit, r.stdout, dirs)];
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.38",
3
+ "version": "0.124.40",
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.38",
70
+ "@intentius/chant": "^0.124.40",
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
+ });
@@ -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
- * Throws when the output is not the rendered config.
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(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");
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
- out.push({
117
- name: typeof raw.name === "string" && raw.name ? raw.name : label,
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
- for (const m of log.matchAll(MOCK_WARNING)) {
286
- const unit = upstreamOfConfigPath(".", m[2]!, dirs);
287
- const upstream = upstreamOfConfigPath(".", m[1]!, dirs);
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)) continue;
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
 
@@ -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, then `output -json` in each upstream they read. Empty
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
- return [unit, parseRenderedDependencies(unit, r.stdout, dirs)];
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
  });