@intentius/chant-lexicon-terraform 0.124.31 → 0.124.33
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.js +111 -0
- package/dist/__fixtures__/terragrunt/record.js +57 -0
- package/dist/behaviour/index.js +6 -0
- package/dist/behaviour/kinds.js +194 -0
- package/dist/behaviour/predict.js +297 -0
- package/dist/behaviour/reference-catalog.js +178 -0
- package/dist/behaviour/request.js +482 -0
- package/dist/change-set.js +234 -0
- package/dist/changed-roots.js +97 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +44 -0
- package/dist/codegen/generate-cli.js +9 -0
- package/dist/codegen/generate.js +59 -0
- package/dist/codegen/package.js +43 -0
- package/dist/commands.js +106 -0
- package/dist/components/capability-plugin.d.ts +38 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +260 -0
- package/dist/composites/terraform-adopt-op.d.ts +2 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/composites/terraform-adopt-op.js +153 -0
- package/dist/composites/terraform-apply-op.d.ts +2 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +184 -0
- package/dist/composites/terraform-pin-rollout-op.js +45 -0
- package/dist/composites/terraform-watch-op.js +219 -0
- package/dist/config.js +118 -0
- package/dist/describe-resources.js +984 -0
- package/dist/generated/index.js +1 -0
- package/dist/hcl/descend.js +208 -0
- package/dist/hcl/edges.js +287 -0
- package/dist/hcl/jsonencode.js +442 -0
- package/dist/hcl/marker.js +212 -0
- package/dist/hcl/parse.js +372 -0
- package/dist/hcl/references.js +156 -0
- package/dist/hcl/roots.js +98 -0
- package/dist/hcl/suppressions.js +189 -0
- package/dist/hcl/value.js +72 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +501 -0
- package/dist/lint/audit-lineage.js +442 -0
- package/dist/lint/post-synth/blocks.js +87 -0
- package/dist/lint/post-synth/fixtures/load.js +94 -0
- package/dist/lint/post-synth/flat-attributes.js +135 -0
- package/dist/lint/post-synth/index.js +90 -0
- package/dist/lint/post-synth/module-source.js +153 -0
- package/dist/lint/post-synth/scope.js +104 -0
- package/dist/lint/post-synth/terragrunt.js +63 -0
- package/dist/lint/post-synth/tf001.js +138 -0
- package/dist/lint/post-synth/tf002.js +143 -0
- package/dist/lint/post-synth/tf003.js +60 -0
- package/dist/lint/post-synth/tf004.js +54 -0
- package/dist/lint/post-synth/tf005.js +68 -0
- package/dist/lint/post-synth/tf006.js +43 -0
- package/dist/lint/post-synth/tf007.js +66 -0
- package/dist/lint/post-synth/tf008.js +64 -0
- package/dist/lint/post-synth/tf009.js +49 -0
- package/dist/lint/post-synth/tf010.js +40 -0
- package/dist/lint/post-synth/tf011.js +38 -0
- package/dist/lint/post-synth/tf012.js +36 -0
- package/dist/lint/post-synth/tf013.js +53 -0
- package/dist/lint/post-synth/tf014.js +61 -0
- package/dist/lint/post-synth/tf015.js +62 -0
- package/dist/lint/post-synth/tf016.js +66 -0
- package/dist/lint/post-synth/tf017.js +39 -0
- package/dist/lint/post-synth/tf018.js +63 -0
- package/dist/lint/post-synth/tf019.js +60 -0
- package/dist/lint/post-synth/tf020.js +103 -0
- package/dist/lint/post-synth/tf021.js +117 -0
- package/dist/lint/post-synth/tf022.js +51 -0
- package/dist/lint/post-synth/tf024.js +62 -0
- package/dist/lint/post-synth/tf025.js +83 -0
- package/dist/lint/post-synth/tf026.js +74 -0
- package/dist/lint/post-synth/tf027.js +66 -0
- package/dist/lint/post-synth/tf028.js +83 -0
- package/dist/lint/post-synth/tf029.js +73 -0
- package/dist/lint/post-synth/tf030.js +253 -0
- package/dist/lint/post-synth/tf031.js +332 -0
- package/dist/lint/post-synth/tf032.js +201 -0
- package/dist/lint/post-synth/tf033.js +126 -0
- package/dist/lint/post-synth/tf034.js +45 -0
- package/dist/lint/post-synth/tf035.js +63 -0
- package/dist/lint/post-synth/tf036.js +97 -0
- package/dist/lint/post-synth/tf037.js +80 -0
- package/dist/lint/post-synth/tf038.js +63 -0
- package/dist/lint/post-synth/tf039.js +55 -0
- package/dist/lint/post-synth/tf040.js +67 -0
- package/dist/lint/post-synth/tf041.js +53 -0
- package/dist/lint/post-synth/tf042.js +38 -0
- package/dist/lint/post-synth/tf043.js +45 -0
- package/dist/lint/post-synth/tf044.js +45 -0
- package/dist/lint/post-synth/tf045.js +44 -0
- package/dist/lint/rules/fixtures/load.js +50 -0
- package/dist/lint/rules/index.js +4 -0
- package/dist/lint/rules/plan-before-apply.js +136 -0
- package/dist/lint/secret-shape.js +149 -0
- package/dist/live-ownership.js +57 -0
- package/dist/lsp/completions.js +95 -0
- package/dist/lsp/context.js +358 -0
- package/dist/lsp/hover.js +45 -0
- package/dist/lsp/option-keys.js +93 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.js +30 -0
- package/dist/op/activities/live-detect.js +143 -0
- package/dist/op/activities/pin-rollout.js +45 -0
- package/dist/op/activities/terraform.js +1270 -0
- package/dist/op/activity-contracts.js +162 -0
- package/dist/op/adoption.js +272 -0
- package/dist/op/builders.js +125 -0
- package/dist/op/resolve-root-mode.js +77 -0
- package/dist/package-cli.js +16 -0
- package/dist/pin/edit-ts.js +76 -0
- package/dist/pin/edit.js +118 -0
- package/dist/pin/forge.js +65 -0
- package/dist/pin/index.js +16 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +382 -0
- package/dist/pin/scan.js +287 -0
- package/dist/pin/source.js +171 -0
- package/dist/pin/waves.js +109 -0
- package/dist/plan-digest.js +67 -0
- package/dist/plugin.js +323 -0
- package/dist/serializer.js +23 -0
- package/dist/state-ownership.js +29 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +290 -0
- package/dist/terragrunt/index.js +10 -0
- package/dist/terragrunt/mocks.js +270 -0
- package/dist/terragrunt/run.js +264 -0
- package/dist/terragrunt/units.js +195 -0
- package/dist/terragrunt/wave.js +125 -0
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +31 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
- package/src/components/capability-plugin.test.ts +97 -2
- package/src/components/capability-plugin.ts +117 -12
- package/src/components/pr-loop.tofu.test.ts +51 -11
- package/src/composites/terraform-adopt-op.test.ts +6 -0
- package/src/composites/terraform-adopt-op.ts +7 -2
- package/src/composites/terraform-apply-op.test.ts +6 -0
- package/src/composites/terraform-apply-op.ts +7 -2
- package/src/index.ts +1 -0
- package/src/pin/rollout.ts +2 -1
- package/src/terragrunt/affected.test.ts +11 -0
- package/src/terragrunt/affected.ts +2 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Terragrunt runner (#3414): discovery and one wave, run against a real
|
|
3
|
+
* `terragrunt`. The pure halves are `./units.ts` and `./wave.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Every call takes an injectable `exec`, so tests stub the process and the
|
|
6
|
+
* acceptance test runs the real binary.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
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";
|
|
14
|
+
import { parseTerragruntReport, terragruntEnv, terragruntUnitResults, terragruntWaveArgs, terragruntWaveParts, } from "./wave.js";
|
|
15
|
+
export const defaultTerragruntExec = (file, args, options) => new Promise((done) => {
|
|
16
|
+
execFile(file, [...args], { cwd: options.cwd, env: { ...process.env, ...options.env }, maxBuffer: 256 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
17
|
+
const code = err ? (typeof err.code === "number" ? (err.code) : null) : 0;
|
|
18
|
+
done({ code, stdout: String(stdout), stderr: err && code === null ? `${String(stderr)}${err.message}` : String(stderr) });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
/** Thrown when `terragrunt` is missing, too old or fails. */
|
|
22
|
+
export class TerragruntError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "TerragruntError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Refuse a Terragrunt older than 1.1. Returns the version line. */
|
|
29
|
+
export async function checkTerragruntVersion(options) {
|
|
30
|
+
const exec = options.exec ?? defaultTerragruntExec;
|
|
31
|
+
const tg = options.terragrunt ?? "terragrunt";
|
|
32
|
+
const r = await exec(tg, ["--version"], { cwd: options.dir, env: {} });
|
|
33
|
+
if (r.code !== 0)
|
|
34
|
+
throw new TerragruntError(`\`${tg} --version\` failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
35
|
+
const problem = terragruntVersionProblem(r.stdout);
|
|
36
|
+
if (problem)
|
|
37
|
+
throw new TerragruntError(problem);
|
|
38
|
+
return r.stdout.trim();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The project's units, from `terragrunt find`, with catalog templates and the
|
|
42
|
+
* module cache left out and `.terragrunt-filters` honoured.
|
|
43
|
+
*/
|
|
44
|
+
export async function discoverTerragruntUnits(options) {
|
|
45
|
+
const exec = options.exec ?? defaultTerragruntExec;
|
|
46
|
+
const tg = options.terragrunt ?? "terragrunt";
|
|
47
|
+
const args = terragruntFindArgs(options);
|
|
48
|
+
const r = await exec(tg, args, { cwd: options.dir, env: terragruntEnv(options.binary) });
|
|
49
|
+
if (r.code !== 0)
|
|
50
|
+
throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}): ${r.stderr.trim()}`);
|
|
51
|
+
let units;
|
|
52
|
+
try {
|
|
53
|
+
units = parseTerragruntFind(r.stdout);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw new TerragruntError(`could not read \`terragrunt find\` output: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
return { units, warnings: options.binary ? terraformBinaryWarnings(options.dir, units, options.binary) : [] };
|
|
59
|
+
}
|
|
60
|
+
const TERRAFORM_BINARY = /^\s*terraform_binary\s*=\s*(.+?)\s*$/m;
|
|
61
|
+
/**
|
|
62
|
+
* A warning per unit whose `terragrunt.hcl`, or a file it includes, sets
|
|
63
|
+
* `terraform_binary` to something other than `binary`. `TG_TF_PATH` wins over
|
|
64
|
+
* it (observed on 1.1.6), so the unit runs `binary`, not what it names.
|
|
65
|
+
*/
|
|
66
|
+
export function terraformBinaryWarnings(dir, units, binary) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const u of units) {
|
|
69
|
+
const files = [join(u.path, "terragrunt.hcl"), ...Object.values(u.include ?? {})];
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = readFileSync(resolve(dir, f), "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const m = TERRAFORM_BINARY.exec(text);
|
|
79
|
+
if (!m)
|
|
80
|
+
continue;
|
|
81
|
+
const literal = /^"([^"$]*)"$/.exec(m[1]);
|
|
82
|
+
if (literal && literal[1] === binary)
|
|
83
|
+
break;
|
|
84
|
+
out.push(`${u.path}: ${f} sets terraform_binary = ${m[1]}, but the project's binary is ${binary}; ` +
|
|
85
|
+
`TG_TF_PATH=${binary} overrides it, so the unit runs ${binary}`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** The planner a wave's plans come from: the project's binary, else Terragrunt's default of tofu. */
|
|
92
|
+
export function terragruntPlanner(binary) {
|
|
93
|
+
return plannerForBinary(binary ?? "tofu");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Thrown when a wave's plans would read `mock_outputs` (#3416). Nothing in
|
|
97
|
+
* the wave was planned, so there is nothing to gate. `reads` names each unit,
|
|
98
|
+
* dependency and upstream.
|
|
99
|
+
*/
|
|
100
|
+
export class TerragruntMockRefusal extends TerragruntError {
|
|
101
|
+
constructor(reads) {
|
|
102
|
+
super(describeMockRefusal(reads));
|
|
103
|
+
this.reads = reads;
|
|
104
|
+
this.name = "TerragruntMockRefusal";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Run `fn` over `items`, at most `limit` at a time, keeping their order. */
|
|
108
|
+
async function pool(items, limit, fn) {
|
|
109
|
+
const out = new Array(items.length);
|
|
110
|
+
let next = 0;
|
|
111
|
+
const worker = async () => {
|
|
112
|
+
while (next < items.length) {
|
|
113
|
+
const i = next++;
|
|
114
|
+
out[i] = await fn(items[i]);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/** `dir` and its real path, for reading the absolute paths Terragrunt prints. */
|
|
121
|
+
function dirsOf(dir) {
|
|
122
|
+
const abs = resolve(dir);
|
|
123
|
+
try {
|
|
124
|
+
const real = realpathSync(abs);
|
|
125
|
+
return real === abs ? [abs] : [abs, real];
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return [abs];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Which dependencies of the wave's units would read `mock_outputs` at plan,
|
|
133
|
+
* asked of Terragrunt: `render --json` in each unit for its evaluated
|
|
134
|
+
* `dependency` blocks, then `output -json` in each upstream they read. Empty
|
|
135
|
+
* when the wave would plan on real outputs only. Throws when Terragrunt
|
|
136
|
+
* cannot render a unit or read an upstream's outputs, since then nobody
|
|
137
|
+
* knows what the plan would read. A unit with no `terragrunt.hcl` in `dir`
|
|
138
|
+
* (removed by the change, so planned as a destroy) is not rendered.
|
|
139
|
+
*/
|
|
140
|
+
export async function checkTerragruntWaveMocks(input) {
|
|
141
|
+
const exec = input.exec ?? defaultTerragruntExec;
|
|
142
|
+
const tg = input.terragrunt ?? "terragrunt";
|
|
143
|
+
const env = terragruntEnv(input.binary);
|
|
144
|
+
const limit = input.parallelism ?? 8;
|
|
145
|
+
const dirs = dirsOf(input.dir);
|
|
146
|
+
// A unit this change removed has no terragrunt.hcl here: Terragrunt plans its destroy from the old checkout.
|
|
147
|
+
// It is not rendered; the plan log check still covers it.
|
|
148
|
+
const units = [...new Set(input.units)].sort().filter((u) => existsSync(join(resolve(input.dir), u, "terragrunt.hcl")));
|
|
149
|
+
const rendered = await pool(units, limit, async (unit) => {
|
|
150
|
+
const args = terragruntRenderArgs(unit);
|
|
151
|
+
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
152
|
+
if (r.code !== 0)
|
|
153
|
+
throw new TerragruntError(`\`${tg} ${args.join(" ")}\` failed (exit ${r.code}), so the wave's dependencies are unknown: ${r.stderr.trim()}`);
|
|
154
|
+
try {
|
|
155
|
+
return [unit, parseRenderedDependencies(unit, r.stdout, dirs)];
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
throw new TerragruntError(`could not read \`terragrunt render --json\` for ${unit}: ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
const dependencies = new Map(rendered);
|
|
162
|
+
const upstreams = terragruntOutputUpstreams(dependencies);
|
|
163
|
+
const outputs = await pool(upstreams, limit, async (upstream) => {
|
|
164
|
+
const args = terragruntOutputArgs(upstream);
|
|
165
|
+
const r = await exec(tg, args, { cwd: input.dir, env });
|
|
166
|
+
const parsed = r.code === 0 ? parseTerragruntOutputs(r.stdout) : undefined;
|
|
167
|
+
if (!parsed) {
|
|
168
|
+
throw new TerragruntError(`could not read the outputs of ${upstream}, which the wave reads (\`${tg} ${args.join(" ")}\` exit ${r.code}): ${(r.stderr || r.stdout).trim().slice(-2000)}`);
|
|
169
|
+
}
|
|
170
|
+
return [upstream, parsed];
|
|
171
|
+
});
|
|
172
|
+
return terragruntMockReads({ dependencies, outputs: new Map(outputs) });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Plan one wave with one `terragrunt run --all`, and read each unit's plan
|
|
176
|
+
* JSON and run-report row.
|
|
177
|
+
*
|
|
178
|
+
* Unless the plan is provisional, the wave is first checked for mock reads
|
|
179
|
+
* ({@link checkTerragruntWaveMocks}) and refused with a
|
|
180
|
+
* {@link TerragruntMockRefusal} when any dependency would read
|
|
181
|
+
* `mock_outputs`; nothing is planned then. A unit the plan log still shows
|
|
182
|
+
* reading a mock (its upstream's state went away in between) becomes a
|
|
183
|
+
* failed member.
|
|
184
|
+
*/
|
|
185
|
+
export async function planTerragruntWave(input) {
|
|
186
|
+
const exec = input.exec ?? defaultTerragruntExec;
|
|
187
|
+
const tg = input.terragrunt ?? "terragrunt";
|
|
188
|
+
const work = resolve(input.dir, input.workDir);
|
|
189
|
+
const outDir = join(work, "plans");
|
|
190
|
+
const jsonOutDir = join(work, "json");
|
|
191
|
+
const reportFile = join(work, "plan-report.json");
|
|
192
|
+
const marker = join(work, PROVISIONAL_MARKER);
|
|
193
|
+
// A plan or report left by an earlier attempt must not stand in for a unit that did not plan this time.
|
|
194
|
+
for (const stale of [outDir, jsonOutDir, reportFile, marker])
|
|
195
|
+
rmSync(stale, { recursive: true, force: true });
|
|
196
|
+
if (!input.provisional) {
|
|
197
|
+
const reads = await checkTerragruntWaveMocks({ ...input, exec });
|
|
198
|
+
if (reads.length > 0)
|
|
199
|
+
throw new TerragruntMockRefusal(reads);
|
|
200
|
+
}
|
|
201
|
+
mkdirSync(work, { recursive: true });
|
|
202
|
+
// Written before planning, so a plan cut short still never applies.
|
|
203
|
+
if (input.provisional)
|
|
204
|
+
writeFileSync(marker, "planned before its upstream applied; never applied\n");
|
|
205
|
+
const args = terragruntWaveArgs({
|
|
206
|
+
units: input.units,
|
|
207
|
+
command: "plan",
|
|
208
|
+
outDir,
|
|
209
|
+
jsonOutDir,
|
|
210
|
+
reportFile,
|
|
211
|
+
...(input.destroy ? { destroy: true } : {}),
|
|
212
|
+
...(input.parallelism !== undefined ? { parallelism: input.parallelism } : {}),
|
|
213
|
+
});
|
|
214
|
+
const r = await exec(tg, args, { cwd: input.dir, env: terragruntEnv(input.binary) });
|
|
215
|
+
const report = existsSync(reportFile) ? parseTerragruntReport(readFileSync(reportFile, "utf8")) : new Map();
|
|
216
|
+
const planFor = (unit) => {
|
|
217
|
+
const f = join(jsonOutDir, unit, "tfplan.json");
|
|
218
|
+
return existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : undefined;
|
|
219
|
+
};
|
|
220
|
+
const parts = terragruntWaveParts({ units: input.units, report, planFor, planner: terragruntPlanner(input.binary) });
|
|
221
|
+
const inWave = new Set(input.units);
|
|
222
|
+
const mocked = terragruntMockWarnings(`${r.stderr}\n${r.stdout}`, dirsOf(input.dir)).filter((m) => inWave.has(m.unit));
|
|
223
|
+
return {
|
|
224
|
+
parts: input.provisional ? markProvisional(parts) : failMockedParts(parts, mocked),
|
|
225
|
+
provisional: input.provisional === true,
|
|
226
|
+
results: terragruntUnitResults(input.units, report),
|
|
227
|
+
code: r.code,
|
|
228
|
+
log: r.stderr,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Apply one planned wave from its saved plans, with the same filters. Refuses
|
|
233
|
+
* before running anything when a unit of the wave has no saved plan, since
|
|
234
|
+
* Terragrunt would plan it afresh and apply a plan nobody saw, and when the
|
|
235
|
+
* plans are provisional.
|
|
236
|
+
*/
|
|
237
|
+
export async function applyTerragruntWave(input) {
|
|
238
|
+
const exec = input.exec ?? defaultTerragruntExec;
|
|
239
|
+
const tg = input.terragrunt ?? "terragrunt";
|
|
240
|
+
const work = resolve(input.dir, input.workDir);
|
|
241
|
+
const outDir = join(work, "plans");
|
|
242
|
+
if (input.provisional || existsSync(join(work, PROVISIONAL_MARKER))) {
|
|
243
|
+
throw new TerragruntError(`the plans under ${work} are provisional: planned before their upstream applied, possibly on mock_outputs. ` +
|
|
244
|
+
`They are never applied; plan the wave again once its upstream applied`);
|
|
245
|
+
}
|
|
246
|
+
const missing = [...new Set(input.units)].sort().filter((u) => !existsSync(join(outDir, u, "tfplan.tfplan")));
|
|
247
|
+
if (missing.length > 0) {
|
|
248
|
+
throw new TerragruntError(`no saved plan for ${missing.join(", ")} under ${outDir}; plan the wave before applying it, so apply runs the plan that was reviewed`);
|
|
249
|
+
}
|
|
250
|
+
const reportFile = join(work, "apply-report.json");
|
|
251
|
+
rmSync(reportFile, { force: true });
|
|
252
|
+
mkdirSync(dirname(reportFile), { recursive: true });
|
|
253
|
+
const args = terragruntWaveArgs({
|
|
254
|
+
units: input.units,
|
|
255
|
+
command: "apply",
|
|
256
|
+
outDir,
|
|
257
|
+
reportFile,
|
|
258
|
+
...(input.destroy ? { destroy: true } : {}),
|
|
259
|
+
...(input.parallelism !== undefined ? { parallelism: input.parallelism } : {}),
|
|
260
|
+
});
|
|
261
|
+
const r = await exec(tg, args, { cwd: input.dir, env: terragruntEnv(input.binary) });
|
|
262
|
+
const report = existsSync(reportFile) ? parseTerragruntReport(readFileSync(reportFile, "utf8")) : new Map();
|
|
263
|
+
return { results: terragruntUnitResults(input.units, report), code: r.code, log: r.stderr };
|
|
264
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terragrunt units as roots (#3414): what a unit is, how discovery finds the
|
|
3
|
+
* units, and how the unit graph orders them into waves.
|
|
4
|
+
*
|
|
5
|
+
* A Terragrunt unit is a root: its own state, plan file and plan digest. The
|
|
6
|
+
* units and their edges come from `terragrunt find --json --dag
|
|
7
|
+
* --dependencies`, run by Terragrunt itself, so chant never evaluates
|
|
8
|
+
* Terragrunt's HCL to learn the graph. Directories that group units (implicit
|
|
9
|
+
* stacks) are labels: a unit's stack is its parent directory.
|
|
10
|
+
*
|
|
11
|
+
* This module runs nothing and imports only chant's `gated-waves` subpath, so
|
|
12
|
+
* terragucci can bundle it without TypeScript (#3421). `./run.ts` spawns.
|
|
13
|
+
*/
|
|
14
|
+
import { layerWaves } from "@intentius/chant/gated-waves";
|
|
15
|
+
/** The oldest Terragrunt this runner drives: `run --all`, `find --dag` and `--filter` as 1.1 ships them. */
|
|
16
|
+
export const TERRAGRUNT_MIN_VERSION = "1.1.0";
|
|
17
|
+
/**
|
|
18
|
+
* Filters every discovery adds: catalog unit templates, which parse only
|
|
19
|
+
* inside a stack, and Terragrunt's module cache. A project's own `exclude`
|
|
20
|
+
* globs are added after these.
|
|
21
|
+
*/
|
|
22
|
+
export const TERRAGRUNT_DISCOVERY_EXCLUDES = ["catalog/**", "**/.terragrunt-cache/**"];
|
|
23
|
+
/** Read `terragrunt --version` output (`terragrunt version v1.1.6`). Undefined when no version is in it. */
|
|
24
|
+
export function parseTerragruntVersion(output) {
|
|
25
|
+
const m = /v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(output);
|
|
26
|
+
if (!m)
|
|
27
|
+
return undefined;
|
|
28
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), ...(m[4] ? { pre: m[4] } : {}), raw: m[0] };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Why this Terragrunt cannot run units, or undefined when it can. A release
|
|
32
|
+
* candidate of 1.1.0 or later counts as that release line.
|
|
33
|
+
*/
|
|
34
|
+
export function terragruntVersionProblem(output) {
|
|
35
|
+
const v = parseTerragruntVersion(output);
|
|
36
|
+
if (!v)
|
|
37
|
+
return `could not read a version from \`terragrunt --version\` (got ${JSON.stringify(output.trim().slice(0, 80))})`;
|
|
38
|
+
const [minMajor, minMinor] = TERRAGRUNT_MIN_VERSION.split(".").map(Number);
|
|
39
|
+
if (v.major > minMajor || (v.major === minMajor && v.minor >= minMinor))
|
|
40
|
+
return undefined;
|
|
41
|
+
return `Terragrunt ${v.raw} is older than ${TERRAGRUNT_MIN_VERSION}, the oldest release chant runs units with (run --all, find --dag and --filter as 1.1 ships them)`;
|
|
42
|
+
}
|
|
43
|
+
/** A path filter that selects exactly one unit. Braced so a path with glob characters stays a path. */
|
|
44
|
+
export function unitPathFilter(path) {
|
|
45
|
+
return `{./${path}}`;
|
|
46
|
+
}
|
|
47
|
+
/** A filter that drops everything under `glob`, relative to where discovery runs. */
|
|
48
|
+
export function excludeFilter(glob) {
|
|
49
|
+
return `!./${glob.replace(/^\.\//, "")}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Arguments to `terragrunt` for discovery. `.terragrunt-filters` is read, as
|
|
53
|
+
* Terragrunt reads it by default; the excludes narrow what it selects.
|
|
54
|
+
*/
|
|
55
|
+
export function terragruntFindArgs(options = {}) {
|
|
56
|
+
const excludes = [...TERRAGRUNT_DISCOVERY_EXCLUDES, ...(options.exclude ?? [])];
|
|
57
|
+
return [
|
|
58
|
+
"find",
|
|
59
|
+
"--json",
|
|
60
|
+
"--dag",
|
|
61
|
+
"--dependencies",
|
|
62
|
+
"--include",
|
|
63
|
+
"--reading",
|
|
64
|
+
"--no-color",
|
|
65
|
+
...excludes.flatMap((g) => ["--filter", excludeFilter(g)]),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
69
|
+
const strings = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string") : []);
|
|
70
|
+
const clean = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
71
|
+
/**
|
|
72
|
+
* The units in `find --json` output, in the order Terragrunt printed them
|
|
73
|
+
* (its DAG order under `--dag`). Stacks and anything else that is not a unit
|
|
74
|
+
* are left out. Throws when the output is not a JSON array.
|
|
75
|
+
*/
|
|
76
|
+
export function parseTerragruntFind(output) {
|
|
77
|
+
const doc = typeof output === "string" ? JSON.parse(output.trim() === "" ? "[]" : output) : output;
|
|
78
|
+
if (!Array.isArray(doc))
|
|
79
|
+
throw new Error("terragrunt find --json did not print an array");
|
|
80
|
+
const units = [];
|
|
81
|
+
for (const raw of doc) {
|
|
82
|
+
if (!isObject(raw) || raw.type !== "unit" || typeof raw.path !== "string")
|
|
83
|
+
continue;
|
|
84
|
+
const include = isObject(raw.include)
|
|
85
|
+
? Object.fromEntries(Object.entries(raw.include).filter((e) => typeof e[1] === "string").map(([k, v]) => [k, clean(v)]))
|
|
86
|
+
: undefined;
|
|
87
|
+
const reading = Array.isArray(raw.reading) ? strings(raw.reading).map(clean) : undefined;
|
|
88
|
+
units.push({
|
|
89
|
+
path: clean(raw.path),
|
|
90
|
+
dependencies: [...new Set(strings(raw.dependencies).map(clean))].sort(),
|
|
91
|
+
...(include && Object.keys(include).length > 0 ? { include } : {}),
|
|
92
|
+
...(reading ? { reading } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return units;
|
|
96
|
+
}
|
|
97
|
+
/** A unit's implicit stack: its parent directory, or `.` for a unit at the top. */
|
|
98
|
+
export function stackOfUnit(path) {
|
|
99
|
+
const i = path.lastIndexOf("/");
|
|
100
|
+
return i <= 0 ? "." : path.slice(0, i);
|
|
101
|
+
}
|
|
102
|
+
/** Whether `path` matches `glob` (`*` within a segment, `**` across segments, `?` one character). */
|
|
103
|
+
export function matchesUnitGlob(path, glob) {
|
|
104
|
+
const g = clean(glob);
|
|
105
|
+
let re = "";
|
|
106
|
+
for (let i = 0; i < g.length; i++) {
|
|
107
|
+
const c = g[i];
|
|
108
|
+
if (c === "*" && g[i + 1] === "*") {
|
|
109
|
+
// `a/**/b` and `a/**` also match zero directories.
|
|
110
|
+
if (g[i + 2] === "/") {
|
|
111
|
+
re += "(?:.*/)?";
|
|
112
|
+
i += 2;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
re += ".*";
|
|
116
|
+
i += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (c === "*")
|
|
120
|
+
re += "[^/]*";
|
|
121
|
+
else if (c === "?")
|
|
122
|
+
re += "[^/]";
|
|
123
|
+
else
|
|
124
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
125
|
+
}
|
|
126
|
+
return new RegExp(`^${re}$`).test(path);
|
|
127
|
+
}
|
|
128
|
+
/** The units, as the nodes gated waves (#3049) and a fan-out (#2417) layer. */
|
|
129
|
+
export function terragruntWaveNodes(units) {
|
|
130
|
+
return units.map((u) => ({ name: u.path, dependsOn: u.dependencies }));
|
|
131
|
+
}
|
|
132
|
+
/** Thrown when units cannot be cut into waves. The message names the units. */
|
|
133
|
+
export class TerragruntWaveError extends Error {
|
|
134
|
+
constructor(message) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.name = "TerragruntWaveError";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Cut units into waves by Terragrunt's unit graph: each unit after every unit
|
|
141
|
+
* it depends on. A dependency outside `units` holds nothing back; it is not
|
|
142
|
+
* part of this change. Each wave is sorted by path.
|
|
143
|
+
*
|
|
144
|
+
* Canaries go first, layered among themselves, so a canary that reads
|
|
145
|
+
* another canary still waits for it to apply. A canary that depends on a unit
|
|
146
|
+
* of the set that is not a canary is refused: it would apply before what it
|
|
147
|
+
* reads.
|
|
148
|
+
*/
|
|
149
|
+
export function terragruntWaves(units, options = {}) {
|
|
150
|
+
const canaries = new Set((options.canary ?? []).flatMap((g) => units.filter((u) => matchesUnitGlob(u.path, g)).map((u) => u.path)));
|
|
151
|
+
const names = new Set(units.map((u) => u.path));
|
|
152
|
+
for (const u of units) {
|
|
153
|
+
if (!canaries.has(u.path))
|
|
154
|
+
continue;
|
|
155
|
+
const outside = u.dependencies.filter((d) => names.has(d) && !canaries.has(d));
|
|
156
|
+
if (outside.length > 0) {
|
|
157
|
+
throw new TerragruntWaveError(`canary ${u.path} depends on ${outside.join(", ")}, which is not a canary, so the canary wave would apply it first`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const nodes = terragruntWaveNodes(units);
|
|
161
|
+
const layer = (subset) => {
|
|
162
|
+
try {
|
|
163
|
+
return layerWaves(subset);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
throw new TerragruntWaveError(err instanceof Error ? err.message : String(err));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return [...layer(nodes.filter((n) => canaries.has(n.name))), ...layer(nodes.filter((n) => !canaries.has(n.name)))];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Every unit that depends on one of `changed`, directly or through other
|
|
173
|
+
* units, not counting `changed` itself. Sorted by path.
|
|
174
|
+
*
|
|
175
|
+
* `find` reports `dependency` and ordering-only `dependencies` edges as one
|
|
176
|
+
* list, so this follows both. A caller that follows `dependency` edges only
|
|
177
|
+
* passes `edges` built from the units' own blocks.
|
|
178
|
+
*/
|
|
179
|
+
export function terragruntDependents(units, changed, edges = (u) => u.dependencies) {
|
|
180
|
+
const seeds = new Set(changed.map(clean));
|
|
181
|
+
const reached = new Set(seeds);
|
|
182
|
+
let grew = true;
|
|
183
|
+
while (grew) {
|
|
184
|
+
grew = false;
|
|
185
|
+
for (const u of units) {
|
|
186
|
+
if (reached.has(u.path))
|
|
187
|
+
continue;
|
|
188
|
+
if (edges(u).some((d) => reached.has(d))) {
|
|
189
|
+
reached.add(u.path);
|
|
190
|
+
grew = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...reached].filter((p) => !seeds.has(p)).sort();
|
|
195
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One wave of Terragrunt units as one `terragrunt run --all` (#3414): the
|
|
3
|
+
* arguments, the environment, the run report and the change-set parts.
|
|
4
|
+
*
|
|
5
|
+
* A wave names its units with explicit path filters, so it plans and applies
|
|
6
|
+
* exactly those units. `.terragrunt-filters` is not read by a wave run:
|
|
7
|
+
* Terragrunt unions its filters with the ones given on the command line
|
|
8
|
+
* (observed on 1.1.6), so a filters file selecting `live/prod/**` would pull
|
|
9
|
+
* every prod unit into every wave. Discovery reads it; the wave's paths came
|
|
10
|
+
* from discovery.
|
|
11
|
+
*
|
|
12
|
+
* Plan writes `<outDir>/<unit>/tfplan.tfplan` and
|
|
13
|
+
* `<jsonOutDir>/<unit>/tfplan.json`; apply reads the saved plans from the
|
|
14
|
+
* same `outDir` with the same filters.
|
|
15
|
+
*
|
|
16
|
+
* Pure: nothing here spawns or reads files, so terragucci can bundle it. It
|
|
17
|
+
* imports `../change-set` and chant's `change-set` types only.
|
|
18
|
+
*/
|
|
19
|
+
import { terraformChangeSetPart } from "../change-set.js";
|
|
20
|
+
import { stackOfUnit, unitPathFilter } from "./units.js";
|
|
21
|
+
/** Arguments to `terragrunt` for one wave. Throws on an empty wave: with no filter, `run --all` runs every unit. */
|
|
22
|
+
export function terragruntWaveArgs(input) {
|
|
23
|
+
if (input.units.length === 0)
|
|
24
|
+
throw new Error("a Terragrunt wave needs at least one unit; with no path filter run --all would run every unit");
|
|
25
|
+
if (input.parallelism !== undefined && (!Number.isInteger(input.parallelism) || input.parallelism < 1)) {
|
|
26
|
+
throw new Error(`parallelism must be a whole number of 1 or more, got ${input.parallelism}`);
|
|
27
|
+
}
|
|
28
|
+
const units = [...new Set(input.units)].sort();
|
|
29
|
+
const args = [
|
|
30
|
+
"run",
|
|
31
|
+
"--all",
|
|
32
|
+
"--non-interactive",
|
|
33
|
+
"--no-color",
|
|
34
|
+
"--no-filters-file",
|
|
35
|
+
...units.flatMap((u) => ["--filter", unitPathFilter(u)]),
|
|
36
|
+
...(input.destroy ? ["--filter-allow-destroy"] : []),
|
|
37
|
+
"--out-dir",
|
|
38
|
+
input.outDir,
|
|
39
|
+
...(input.command === "plan" && input.jsonOutDir ? ["--json-out-dir", input.jsonOutDir] : []),
|
|
40
|
+
"--report-file",
|
|
41
|
+
input.reportFile,
|
|
42
|
+
"--report-format",
|
|
43
|
+
"json",
|
|
44
|
+
...(input.parallelism !== undefined ? ["--parallelism", String(input.parallelism)] : []),
|
|
45
|
+
"--",
|
|
46
|
+
input.command,
|
|
47
|
+
...(input.command === "plan" && input.destroy ? ["-destroy"] : []),
|
|
48
|
+
...(input.extra ?? []),
|
|
49
|
+
];
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The environment a wave adds: `TG_TF_PATH` when the project names a binary,
|
|
54
|
+
* nothing otherwise, so Terragrunt's own choice (tofu, then terraform, or a
|
|
55
|
+
* unit's `terraform_binary`) stands.
|
|
56
|
+
*/
|
|
57
|
+
export function terragruntEnv(binary) {
|
|
58
|
+
return binary ? { TG_TF_PATH: binary } : {};
|
|
59
|
+
}
|
|
60
|
+
/** The rows of a run report, keyed by unit path. Malformed rows are dropped. */
|
|
61
|
+
export function parseTerragruntReport(text) {
|
|
62
|
+
const doc = typeof text === "string" ? JSON.parse(text.trim() === "" ? "[]" : text) : text;
|
|
63
|
+
const rows = Array.isArray(doc) ? doc : [];
|
|
64
|
+
const out = new Map();
|
|
65
|
+
for (const raw of rows) {
|
|
66
|
+
if (raw === null || typeof raw !== "object")
|
|
67
|
+
continue;
|
|
68
|
+
const r = raw;
|
|
69
|
+
if (typeof r.Name !== "string" || typeof r.Result !== "string")
|
|
70
|
+
continue;
|
|
71
|
+
const name = r.Name.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
72
|
+
out.set(name, {
|
|
73
|
+
name,
|
|
74
|
+
result: r.Result,
|
|
75
|
+
...(typeof r.Reason === "string" && r.Reason ? { reason: r.Reason } : {}),
|
|
76
|
+
...(typeof r.Cause === "string" && r.Cause ? { cause: r.Cause.trim() } : {}),
|
|
77
|
+
...(typeof r.Cmd === "string" ? { cmd: r.Cmd } : {}),
|
|
78
|
+
...(typeof r.Started === "string" ? { started: r.Started } : {}),
|
|
79
|
+
...(typeof r.Ended === "string" ? { ended: r.Ended } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** Each of the wave's units against the run report. A unit the report leaves out failed: nothing says it ran. */
|
|
85
|
+
export function terragruntUnitResults(units, report) {
|
|
86
|
+
return [...new Set(units)].sort().map((unit) => {
|
|
87
|
+
const row = report.get(unit);
|
|
88
|
+
if (!row)
|
|
89
|
+
return { unit, status: "failed", result: "not run", error: "not in Terragrunt's run report, so it did not run" };
|
|
90
|
+
if (row.result === "succeeded")
|
|
91
|
+
return { unit, status: "succeeded", result: row.result };
|
|
92
|
+
const why = [row.reason, row.cause].filter(Boolean).join(": ");
|
|
93
|
+
return { unit, status: "failed", result: row.result, error: `${row.result}${why ? ` (${why})` : ""}` };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The change-set parts of a planned wave: one member per unit, named by the
|
|
98
|
+
* unit's path. A unit the run report marks succeeded, with a plan JSON, goes
|
|
99
|
+
* through {@link terraformChangeSetPart}; any other is a failed member
|
|
100
|
+
* carrying the report's result, so a unit that never planned cannot read as
|
|
101
|
+
* planned with nothing to do.
|
|
102
|
+
*/
|
|
103
|
+
export function terragruntWaveParts(input) {
|
|
104
|
+
const scopeFor = input.scopeFor ?? stackOfUnit;
|
|
105
|
+
return terragruntUnitResults(input.units, input.report).map((r) => {
|
|
106
|
+
const scope = scopeFor(r.unit);
|
|
107
|
+
const plan = r.status === "succeeded" ? input.planFor(r.unit) : undefined;
|
|
108
|
+
if (r.status === "succeeded" && plan !== undefined) {
|
|
109
|
+
return terraformChangeSetPart({ member: r.unit, plan, planner: input.planner, ...(scope ? { scope } : {}) });
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
member: {
|
|
113
|
+
member: r.unit,
|
|
114
|
+
lexicon: "terraform",
|
|
115
|
+
planner: input.planner,
|
|
116
|
+
...(scope ? { scope } : {}),
|
|
117
|
+
status: "failed",
|
|
118
|
+
error: r.error ?? "Terragrunt reported the unit succeeded but wrote no plan JSON for it",
|
|
119
|
+
planDigest: null,
|
|
120
|
+
holes: [],
|
|
121
|
+
},
|
|
122
|
+
entries: [],
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate generated lexicon-terraform artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the core validation framework
|
|
5
|
+
* with terraform-specific configuration.
|
|
6
|
+
*/
|
|
7
|
+
import { dirname } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { validateLexiconArtifacts } from "@intentius/chant/codegen/validate";
|
|
10
|
+
/**
|
|
11
|
+
* Empty, and correct empty (#2220). `requiredNames` names generated resource
|
|
12
|
+
* classes that must survive a regenerate, and this lexicon generates none:
|
|
13
|
+
* `codegen/generate.ts` writes `lexicon-terraform.json` as `{}` on purpose,
|
|
14
|
+
* because Terraform's resource surface lives in provider registries rather
|
|
15
|
+
* than in one upstream schema, and an estate's `.tf` files are read as they
|
|
16
|
+
* are. There is no name to require, so the list stays empty and the
|
|
17
|
+
* remaining checks (the artifacts exist, the registry parses, the surface
|
|
18
|
+
* snapshot) are what validate actually proves here.
|
|
19
|
+
*/
|
|
20
|
+
const REQUIRED_NAMES = [];
|
|
21
|
+
/**
|
|
22
|
+
* Validate the generated lexicon-terraform artifacts.
|
|
23
|
+
*/
|
|
24
|
+
export async function validate(opts) {
|
|
25
|
+
const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
26
|
+
return validateLexiconArtifacts({
|
|
27
|
+
lexiconJsonFilename: "lexicon-terraform.json",
|
|
28
|
+
requiredNames: REQUIRED_NAMES,
|
|
29
|
+
basePath,
|
|
30
|
+
});
|
|
31
|
+
}
|