@intentius/chant-lexicon-terraform 0.124.31 → 0.124.32
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.js +152 -0
- package/dist/composites/terraform-apply-op.js +183 -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/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,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one shared HCL parse for the terraform lexicon.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points, one entity shape. `parseTerraformRootDir` reads the `.tf`
|
|
5
|
+
* files of a directory (non-recursive, matching Terraform's own module
|
|
6
|
+
* scoping) and is what `buildRoots()` calls. `parseTerraformRootContent` takes
|
|
7
|
+
* the joined-file string `chant audit` hands a lexicon for a discovered root
|
|
8
|
+
* module: every `.tf` in filename order behind a `# file: <name>` line comment.
|
|
9
|
+
* Both funnel through `blocksToEntities`, so an entity the audit path sees is
|
|
10
|
+
* the entity the build path sees.
|
|
11
|
+
*
|
|
12
|
+
* Core owns the parser glue: `loadHcl2json()` lazy-loads `@cdktn/hcl2json`
|
|
13
|
+
* (a ~1.8 MB wasm blob) and raises a one-line install hint when it is absent.
|
|
14
|
+
* `parseTerraformDir()` next to it is NOT reused: it returns carve's `TfGraph`,
|
|
15
|
+
* a scoring-and-excision shape with no room for the per-block bodies a
|
|
16
|
+
* serializer and the post-synth checks read. The import path is the wildcard
|
|
17
|
+
* core's exports map already carries (`"./*"` to `./src/*.ts`), so nothing
|
|
18
|
+
* changed in `packages/core/package.json` for this.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { loadHcl2json } from "@intentius/chant/terraform/hcl2json";
|
|
23
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
24
|
+
import { scanSuppressions, directivesFor } from "./suppressions.js";
|
|
25
|
+
/** `entityType` per block kind. */
|
|
26
|
+
export const TERRAFORM_TYPE = "Terraform::Terraform";
|
|
27
|
+
export const PROVIDER_TYPE = "Terraform::Provider";
|
|
28
|
+
export const RESOURCE_TYPE = "Terraform::Resource";
|
|
29
|
+
export const DATA_TYPE = "Terraform::Data";
|
|
30
|
+
export const MODULE_TYPE = "Terraform::Module";
|
|
31
|
+
export const VARIABLE_TYPE = "Terraform::Variable";
|
|
32
|
+
export const OUTPUT_TYPE = "Terraform::Output";
|
|
33
|
+
export const LOCALS_TYPE = "Terraform::Locals";
|
|
34
|
+
/** A `live { estate = "..." }` block, or an `estate.chdf.hcl` sidecar's content (#2103). */
|
|
35
|
+
export const LIVE_TYPE = "Terraform::Live";
|
|
36
|
+
/**
|
|
37
|
+
* Terragrunt config entities (#3417). A `terragrunt.hcl` or `root.hcl` is not
|
|
38
|
+
* a Terraform module, so its blocks are read into these types and never into
|
|
39
|
+
* `TERRAFORM_TYPE`: TF001's `required_version` check and the rest of the
|
|
40
|
+
* module rules keep to the `.tf` files.
|
|
41
|
+
*/
|
|
42
|
+
export const TERRAGRUNT_CONFIG_TYPE = "Terraform::TerragruntConfig";
|
|
43
|
+
export const TERRAGRUNT_SOURCE_TYPE = "Terraform::TerragruntSource";
|
|
44
|
+
export const TERRAGRUNT_DEPENDENCY_TYPE = "Terraform::TerragruntDependency";
|
|
45
|
+
export const TERRAGRUNT_REMOTE_STATE_TYPE = "Terraform::TerragruntRemoteState";
|
|
46
|
+
export const TERRAGRUNT_GENERATE_TYPE = "Terraform::TerragruntGenerate";
|
|
47
|
+
/** The Terragrunt config files read, by their default names. A `--config` rename is not followed. */
|
|
48
|
+
export const TERRAGRUNT_FILENAMES = ["root.hcl", "terragrunt.hcl"];
|
|
49
|
+
/** Whether `name` (a bare filename) is a Terragrunt config file this lexicon reads. */
|
|
50
|
+
export function isTerragruntFileName(name) {
|
|
51
|
+
return TERRAGRUNT_FILENAMES.includes(name);
|
|
52
|
+
}
|
|
53
|
+
/** The sidecar filename choudoufu reads an estate declaration from when no in-block `live { }` is used. */
|
|
54
|
+
export const LIVE_SIDECAR_FILENAME = "estate.chdf.hcl";
|
|
55
|
+
/** The expansion meta-argument a block body carries, if any. `count` wins if both are present (Terraform rejects that combination anyway). */
|
|
56
|
+
function expansionOf(body) {
|
|
57
|
+
if ("count" in body)
|
|
58
|
+
return "count";
|
|
59
|
+
if ("for_each" in body)
|
|
60
|
+
return "for_each";
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build one entity. Written out rather than run through `createResource`
|
|
65
|
+
* from `@intentius/chant/runtime`: that factory is for generated resource
|
|
66
|
+
* classes and hides `props` behind a non-enumerable descriptor plus an
|
|
67
|
+
* `attrMap` of `AttrRef`s this lexicon has nothing to put in. The literal
|
|
68
|
+
* carries the same marker, so `isDeclarable()` and `isResourceDeclarable()`
|
|
69
|
+
* both hold.
|
|
70
|
+
*/
|
|
71
|
+
export function terraformEntity(entityType, address, body, file, root, extra, source = "", line, suppressions) {
|
|
72
|
+
const expansion = expansionOf(body);
|
|
73
|
+
return {
|
|
74
|
+
[DECLARABLE_MARKER]: true,
|
|
75
|
+
lexicon: "terraform",
|
|
76
|
+
entityType,
|
|
77
|
+
kind: "resource",
|
|
78
|
+
stack: root,
|
|
79
|
+
props: {
|
|
80
|
+
address,
|
|
81
|
+
body,
|
|
82
|
+
file,
|
|
83
|
+
root,
|
|
84
|
+
source,
|
|
85
|
+
line,
|
|
86
|
+
...(expansion ? { expansion } : {}),
|
|
87
|
+
...(extra?.mode !== undefined ? { mode: extra.mode } : {}),
|
|
88
|
+
...(extra?.estate !== undefined ? { estate: extra.estate } : {}),
|
|
89
|
+
...(extra?.workspace !== undefined ? { workspace: extra.workspace } : {}),
|
|
90
|
+
...(extra?.delete !== undefined ? { delete: extra.delete } : {}),
|
|
91
|
+
...(extra?.configEstate !== undefined ? { configEstate: extra.configEstate } : {}),
|
|
92
|
+
...(extra?.callers !== undefined && extra.callers.length > 0 ? { callers: extra.callers } : {}),
|
|
93
|
+
},
|
|
94
|
+
suppressions,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** The `# file: <name>` boundary `chant audit` writes between joined `.tf` files. */
|
|
98
|
+
const FILE_MARKER = /^# file: (.+)$/;
|
|
99
|
+
/**
|
|
100
|
+
* Split the joined form back into files. A string with no marker at all is one
|
|
101
|
+
* anonymous file, so a caller that hands over a single `.tf` still parses.
|
|
102
|
+
*/
|
|
103
|
+
export function splitBundleContent(content, fallbackName = "main.tf") {
|
|
104
|
+
const files = [];
|
|
105
|
+
let current;
|
|
106
|
+
for (const line of content.split("\n")) {
|
|
107
|
+
const marker = FILE_MARKER.exec(line);
|
|
108
|
+
if (marker) {
|
|
109
|
+
current = { name: marker[1].trim(), source: "" };
|
|
110
|
+
files.push(current);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!current) {
|
|
114
|
+
current = { name: fallbackName, source: "" };
|
|
115
|
+
files.push(current);
|
|
116
|
+
}
|
|
117
|
+
current.source += current.source === "" ? line : `\n${line}`;
|
|
118
|
+
}
|
|
119
|
+
return files.filter((f) => f.source.trim() !== "");
|
|
120
|
+
}
|
|
121
|
+
/** Every `.tf` directly under `dir`, in filename order. Non-recursive. */
|
|
122
|
+
export function listTerraformFiles(dir) {
|
|
123
|
+
return readdirSync(dir)
|
|
124
|
+
.filter((f) => f.endsWith(".tf"))
|
|
125
|
+
.sort();
|
|
126
|
+
}
|
|
127
|
+
/** The Terragrunt config files directly under `dir` (`root.hcl`, `terragrunt.hcl`), in filename order like the audit's bundle. */
|
|
128
|
+
export function listTerragruntFiles(dir) {
|
|
129
|
+
return TERRAGRUNT_FILENAMES.filter((name) => existsSync(join(dir, name)));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read the `estate.chdf.hcl` sidecar beside `dir`'s `.tf` files, if present.
|
|
133
|
+
* Sibling to {@link listTerraformFiles}: the sidecar is plain HCL (a bare
|
|
134
|
+
* `estate = "..."` attribute, no wrapper block) and is never a `.tf` file, so
|
|
135
|
+
* it falls outside that glob and needs its own read.
|
|
136
|
+
*/
|
|
137
|
+
export function readLiveSidecarFile(dir) {
|
|
138
|
+
const path = join(dir, LIVE_SIDECAR_FILENAME);
|
|
139
|
+
if (!existsSync(path))
|
|
140
|
+
return undefined;
|
|
141
|
+
return { name: LIVE_SIDECAR_FILENAME, source: readFileSync(path, "utf-8") };
|
|
142
|
+
}
|
|
143
|
+
function asArray(value) {
|
|
144
|
+
return Array.isArray(value) ? value : [];
|
|
145
|
+
}
|
|
146
|
+
function asRecord(value) {
|
|
147
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
148
|
+
? value
|
|
149
|
+
: {};
|
|
150
|
+
}
|
|
151
|
+
/** Every `live { ... }` block nested inside every `terraform { ... }` block of `tree`. */
|
|
152
|
+
function liveBlocksIn(tree) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const tfBody of asArray(tree["terraform"])) {
|
|
155
|
+
const body = asRecord(tfBody);
|
|
156
|
+
for (const liveBody of asArray(body["live"]))
|
|
157
|
+
out.push(asRecord(liveBody));
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/** The lock file `terraform init` writes beside a root's `.tf` files. */
|
|
162
|
+
export const LOCK_FILENAME = ".terraform.lock.hcl";
|
|
163
|
+
/**
|
|
164
|
+
* Parse a set of files into entities keyed `<root>/<address>`. Two blocks that
|
|
165
|
+
* genuinely share an address (two `locals` blocks, or the same address in two
|
|
166
|
+
* files of one root) are numbered `~2`, `~3` rather than overwriting.
|
|
167
|
+
*
|
|
168
|
+
* Recognises both ways an estate is declared: a `live { }` block nested in a
|
|
169
|
+
* `terraform { }` block of any file here, and, when `sidecar` is given, the
|
|
170
|
+
* `estate.chdf.hcl` sidecar's bare `estate = "..."` attribute. Either becomes
|
|
171
|
+
* a {@link LIVE_TYPE} entity, and the root is live (`mode: "live"`) exactly
|
|
172
|
+
* when `modeOptions.binary` is `"choudoufu"` and one of the two named an
|
|
173
|
+
* estate; that verdict, and the estate name when live, is then stamped onto
|
|
174
|
+
* every entity this call returns, sidecar and in-block declaration alike so
|
|
175
|
+
* a post-synth check or `describeResources()` reads it without re-parsing.
|
|
176
|
+
* When both forms are present the sidecar wins, matching choudoufu's own
|
|
177
|
+
* "the sidecar is the leading one" (choudoufu refuses the combination
|
|
178
|
+
* outright; this lexicon does not police that here, see TF024/TF025 for
|
|
179
|
+
* what it does police on a live root).
|
|
180
|
+
*
|
|
181
|
+
* `callers` is the module-call chain these files were reached through
|
|
182
|
+
* (chant #2112), empty for a root module. When it is non-empty the keys
|
|
183
|
+
* become `<root>/module.<name>/<address>` (one segment per caller) and every
|
|
184
|
+
* entity carries the chain on `props.callers`. `./descend.ts` is what fills
|
|
185
|
+
* it in; nothing else should pass it.
|
|
186
|
+
*/
|
|
187
|
+
export async function blocksToEntities(files, root, hcl2json, modeOptions, sidecar, callers = []) {
|
|
188
|
+
const parser = hcl2json ?? (await loadHcl2json());
|
|
189
|
+
const entities = new Map();
|
|
190
|
+
const prefix = callers.length > 0 ? `${root}/${callers.join("/")}` : root;
|
|
191
|
+
const add = (entityType, address, body, file, scan, scanAddress = address) => {
|
|
192
|
+
const { line, suppressions } = directivesFor(scan, scanAddress);
|
|
193
|
+
const entity = terraformEntity(entityType, address, (typeof body === "object" && body !== null ? body : {}), file.name, root, {
|
|
194
|
+
...(modeOptions?.workspace !== undefined ? { workspace: modeOptions.workspace } : {}),
|
|
195
|
+
...(modeOptions?.delete !== undefined ? { delete: modeOptions.delete } : {}),
|
|
196
|
+
...(modeOptions?.configEstate !== undefined ? { configEstate: modeOptions.configEstate } : {}),
|
|
197
|
+
...(callers.length > 0 ? { callers } : {}),
|
|
198
|
+
}, file.source, line, suppressions);
|
|
199
|
+
let key = `${prefix}/${address}`;
|
|
200
|
+
for (let n = 2; entities.has(key); n++)
|
|
201
|
+
key = `${prefix}/${address}~${n}`;
|
|
202
|
+
entities.set(key, entity);
|
|
203
|
+
};
|
|
204
|
+
/** `terraform` and `locals` carry no labels: the tree holds a bare body array. */
|
|
205
|
+
const unlabelled = (tree, section, entityType, file, scan) => {
|
|
206
|
+
for (const body of asArray(tree[section]))
|
|
207
|
+
add(entityType, section, body, file, scan);
|
|
208
|
+
};
|
|
209
|
+
/** `provider`, `module`, `variable`, `output`: one label, so `Record<name, body[]>`. */
|
|
210
|
+
const oneLabel = (tree, section, entityType, address, file, scan) => {
|
|
211
|
+
for (const [name, bodies] of Object.entries(asRecord(tree[section]))) {
|
|
212
|
+
for (const body of asArray(bodies))
|
|
213
|
+
add(entityType, address(name), body, file, scan);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/** `resource`, `data`: two labels, so `Record<type, Record<name, body[]>>`. */
|
|
217
|
+
const twoLabels = (tree, section, entityType, address, file, scan) => {
|
|
218
|
+
for (const [type, named] of Object.entries(asRecord(tree[section]))) {
|
|
219
|
+
for (const [name, bodies] of Object.entries(asRecord(named))) {
|
|
220
|
+
for (const body of asArray(bodies))
|
|
221
|
+
add(entityType, address(type, name), body, file, scan);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
for (const file of files) {
|
|
226
|
+
const tree = (await parser.parse(file.name, file.source));
|
|
227
|
+
const scan = scanSuppressions(file.name, file.source);
|
|
228
|
+
if (isTerragruntFileName(file.name)) {
|
|
229
|
+
terragruntBlocks(tree, file, scan, add);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
unlabelled(tree, "terraform", TERRAFORM_TYPE, file, scan);
|
|
233
|
+
for (const liveBody of liveBlocksIn(tree))
|
|
234
|
+
add(LIVE_TYPE, "live", liveBody, file, scan);
|
|
235
|
+
unlabelled(tree, "locals", LOCALS_TYPE, file, scan);
|
|
236
|
+
oneLabel(tree, "provider", PROVIDER_TYPE, (n) => `provider.${n}`, file, scan);
|
|
237
|
+
oneLabel(tree, "module", MODULE_TYPE, (n) => `module.${n}`, file, scan);
|
|
238
|
+
oneLabel(tree, "variable", VARIABLE_TYPE, (n) => `var.${n}`, file, scan);
|
|
239
|
+
oneLabel(tree, "output", OUTPUT_TYPE, (n) => `output.${n}`, file, scan);
|
|
240
|
+
twoLabels(tree, "resource", RESOURCE_TYPE, (t, n) => `${t}.${n}`, file, scan);
|
|
241
|
+
twoLabels(tree, "data", DATA_TYPE, (t, n) => `data.${t}.${n}`, file, scan);
|
|
242
|
+
}
|
|
243
|
+
if (sidecar) {
|
|
244
|
+
const tree = (await parser.parse(sidecar.name, sidecar.source));
|
|
245
|
+
if (typeof tree["estate"] === "string")
|
|
246
|
+
add(LIVE_TYPE, "live", tree, sidecar, scanSuppressions(sidecar.name, sidecar.source));
|
|
247
|
+
}
|
|
248
|
+
// Which of the (at most two) Live entities names the estate, preferring the
|
|
249
|
+
// sidecar over an in-block declaration when both are present.
|
|
250
|
+
let estate;
|
|
251
|
+
for (const entity of entities.values()) {
|
|
252
|
+
if (entity.entityType !== LIVE_TYPE)
|
|
253
|
+
continue;
|
|
254
|
+
const props = entity.props;
|
|
255
|
+
const name = typeof props.body["estate"] === "string" ? props.body["estate"] : undefined;
|
|
256
|
+
if (name === undefined)
|
|
257
|
+
continue;
|
|
258
|
+
if (estate === undefined || props.file === LIVE_SIDECAR_FILENAME)
|
|
259
|
+
estate = name;
|
|
260
|
+
}
|
|
261
|
+
const mode = modeOptions?.binary === "choudoufu" && estate !== undefined ? "live" : "state";
|
|
262
|
+
for (const [key, entity] of entities) {
|
|
263
|
+
const te = entity;
|
|
264
|
+
const stamped = {
|
|
265
|
+
...te,
|
|
266
|
+
props: {
|
|
267
|
+
...te.props,
|
|
268
|
+
mode,
|
|
269
|
+
...(mode === "live" ? { estate } : {}),
|
|
270
|
+
...(modeOptions?.lockFile !== undefined ? { lockFile: modeOptions.lockFile } : {}),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
entities.set(key, stamped);
|
|
274
|
+
}
|
|
275
|
+
return entities;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* One Terragrunt config file's blocks, as entities (#3417): the file's own
|
|
279
|
+
* settings (`terragrunt_version_constraint` and the `include` labels), the
|
|
280
|
+
* `terraform` block that names the unit's source, `remote_state`, each
|
|
281
|
+
* `dependency` and each `generate`. Everything else in the file (`inputs`,
|
|
282
|
+
* `locals`, `iam_role`) is left unread.
|
|
283
|
+
*/
|
|
284
|
+
function terragruntBlocks(tree, file, scan, add) {
|
|
285
|
+
const config = { includes: Object.keys(asRecord(tree["include"])) };
|
|
286
|
+
if (typeof tree["terragrunt_version_constraint"] === "string") {
|
|
287
|
+
config["terragrunt_version_constraint"] = tree["terragrunt_version_constraint"];
|
|
288
|
+
}
|
|
289
|
+
add(TERRAGRUNT_CONFIG_TYPE, "terragrunt", config, file, scan, "");
|
|
290
|
+
for (const body of asArray(tree["terraform"]))
|
|
291
|
+
add(TERRAGRUNT_SOURCE_TYPE, "terragrunt.terraform", body, file, scan, "terraform");
|
|
292
|
+
for (const body of asArray(tree["remote_state"]))
|
|
293
|
+
add(TERRAGRUNT_REMOTE_STATE_TYPE, "remote_state", body, file, scan);
|
|
294
|
+
for (const [name, bodies] of Object.entries(asRecord(tree["dependency"]))) {
|
|
295
|
+
for (const body of asArray(bodies))
|
|
296
|
+
add(TERRAGRUNT_DEPENDENCY_TYPE, `dependency.${name}`, body, file, scan);
|
|
297
|
+
}
|
|
298
|
+
for (const [name, bodies] of Object.entries(asRecord(tree["generate"]))) {
|
|
299
|
+
for (const body of asArray(bodies))
|
|
300
|
+
add(TERRAGRUNT_GENERATE_TYPE, `generate.${name}`, body, file, scan);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parse a root module directory. Reads every `.tf` directly under `dir`, plus
|
|
305
|
+
* the `estate.chdf.hcl` sidecar beside them when present. Throws whatever the
|
|
306
|
+
* parser throws for malformed HCL; `buildRoots()` is where that becomes a
|
|
307
|
+
* warning.
|
|
308
|
+
*/
|
|
309
|
+
export async function parseTerraformRootDir(dir, root, hcl2json, modeOptions, callers = []) {
|
|
310
|
+
// A child module is a Terraform module, never a Terragrunt unit (#3417).
|
|
311
|
+
const names = [...listTerraformFiles(dir), ...(callers.length === 0 ? listTerragruntFiles(dir) : [])];
|
|
312
|
+
const files = names.map((name) => ({
|
|
313
|
+
name,
|
|
314
|
+
source: readFileSync(join(dir, name), "utf-8"),
|
|
315
|
+
}));
|
|
316
|
+
// A child module gets no sidecar read: `estate.chdf.hcl` declares the
|
|
317
|
+
// estate a ROOT runs against, and choudoufu reads it from the root's own
|
|
318
|
+
// directory only. See `./descend.ts`.
|
|
319
|
+
const sidecar = callers.length === 0 ? readLiveSidecarFile(dir) : undefined;
|
|
320
|
+
// Only the root's own directory carries a lock file that matters (#3190).
|
|
321
|
+
const opts = callers.length === 0 ? { ...modeOptions, lockFile: existsSync(join(dir, LOCK_FILENAME)) } : modeOptions;
|
|
322
|
+
return blocksToEntities(files, root, hcl2json, opts, sidecar, callers);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Parse the joined-file string form `chant audit` produces for a discovered
|
|
326
|
+
* root module (`AuditInput.content`). `auditEntities()` is what calls it; that
|
|
327
|
+
* hook's single-argument contract carries no project config, so `modeOptions`
|
|
328
|
+
* is left undefined there and every entity gets `mode: "state"`; the audit
|
|
329
|
+
* path does not detect live mode (#2103). `modeOptions` exists here mainly for
|
|
330
|
+
* tests exercising the mode-stamping behaviour directly against inline HCL,
|
|
331
|
+
* the same way `parseTerraformRootDir`'s callers exercise it against files.
|
|
332
|
+
*/
|
|
333
|
+
export async function parseTerraformRootContent(content, root, hcl2json, modeOptions) {
|
|
334
|
+
return blocksToEntities(splitBundleContent(content), root, hcl2json, modeOptions);
|
|
335
|
+
}
|
|
336
|
+
/** The root name an audit of the audited directory itself parses under. */
|
|
337
|
+
export const AUDIT_ROOT_NAME = "audit-root";
|
|
338
|
+
/**
|
|
339
|
+
* The root name one `chant audit` input parses under (#2217).
|
|
340
|
+
*
|
|
341
|
+
* A build names its roots from `terraform.roots`; an audit has no such
|
|
342
|
+
* config, so the name comes from the input's own path, which is the one thing
|
|
343
|
+
* that tells two discovered roots apart. `envs/prod` becomes `envs.prod`,
|
|
344
|
+
* because an entity key is `<root>/<address>` with the module chain between
|
|
345
|
+
* them and a `/` inside the root name would read as a module scope. The
|
|
346
|
+
* audited directory itself keeps the name {@link AUDIT_ROOT_NAME}, so a
|
|
347
|
+
* single-root audit reads the way it did before an input path was available.
|
|
348
|
+
*/
|
|
349
|
+
export function auditRootName(path) {
|
|
350
|
+
const trimmed = (path ?? "").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
351
|
+
if (trimmed === "" || trimmed === ".")
|
|
352
|
+
return AUDIT_ROOT_NAME;
|
|
353
|
+
return trimmed.replace(/\//g, ".");
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The module scope an entity key names: the part before the address.
|
|
357
|
+
*
|
|
358
|
+
* `"app/aws_s3_bucket.assets"` is scope `"app"` (the root module);
|
|
359
|
+
* `"app/module.cdn/aws_s3_bucket.assets"` is scope `"app/module.cdn"`. Keys
|
|
360
|
+
* are built here (see {@link blocksToEntities}), so the split lives here too:
|
|
361
|
+
* the reference index (`./references.ts`) and the module-scoped checks read
|
|
362
|
+
* scopes through it rather than re-deriving the key shape.
|
|
363
|
+
*/
|
|
364
|
+
export function scopeOfKey(key) {
|
|
365
|
+
const slash = key.lastIndexOf("/");
|
|
366
|
+
return slash === -1 ? "" : key.slice(0, slash);
|
|
367
|
+
}
|
|
368
|
+
/** The `module.<name>` chain in an entity key, outermost first. Empty for a root-module block. */
|
|
369
|
+
export function callersOfKey(key) {
|
|
370
|
+
const parts = key.split("/");
|
|
371
|
+
return parts.length <= 2 ? [] : parts.slice(1, -1);
|
|
372
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declarations does a module scope actually reference (chant #2112)?
|
|
3
|
+
*
|
|
4
|
+
* One pass over every entity's body, collecting the reference forms Terraform
|
|
5
|
+
* writes as expressions: `var.<name>`, `local.<name>`, `data.<type>.<name>`,
|
|
6
|
+
* `module.<name>`, and the `provider = <type>.<alias>` / `providers = { <type>
|
|
7
|
+
* = <type>.<alias> }` meta-arguments. The result is keyed per module scope
|
|
8
|
+
* (`./parse.ts`'s `scopeOfKey`), because Terraform's own namespaces are: a
|
|
9
|
+
* `var.region` inside `module.cdn` is the child module's variable, never the
|
|
10
|
+
* root's, and a rule that pooled the two would call a genuinely unused root
|
|
11
|
+
* variable used.
|
|
12
|
+
*
|
|
13
|
+
* ## It is a string scan, and the rules built on it say so
|
|
14
|
+
*
|
|
15
|
+
* hcl2json gives back an expression as a STRING, with the `${...}` wrapper
|
|
16
|
+
* preserved: `count = length(var.subnets)` arrives as `"${length(var.subnets)}"`
|
|
17
|
+
* and `bucket = var.name` as `"${var.name}"`. There is no expression tree to
|
|
18
|
+
* walk, so this scans the strings with regexes. That reads a reference inside
|
|
19
|
+
* a comment-free body correctly and is deliberately generous at the edges: a
|
|
20
|
+
* name that appears inside a quoted literal (`description = "set var.region"`)
|
|
21
|
+
* counts as a reference, and a reference built dynamically
|
|
22
|
+
* (`lookup(local.by_env, terraform.workspace)` reaching a value nothing names
|
|
23
|
+
* directly) does not. Both directions are why TF020 is report-only and credits
|
|
24
|
+
* tflint's `terraform_unused_declarations` as `overlaps` rather than
|
|
25
|
+
* `equivalent`: it is the same question answered with a coarser instrument.
|
|
26
|
+
*
|
|
27
|
+
* The index records WHICH entity each reference came from, so a check can ask
|
|
28
|
+
* "is anything other than the declaration itself referring to this?". A
|
|
29
|
+
* `variable "port"` whose own `validation` block reads `var.port` is the case
|
|
30
|
+
* that needs it: without the exclusion every validated variable would look
|
|
31
|
+
* used.
|
|
32
|
+
*/
|
|
33
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
34
|
+
import { scopeOfKey } from "./parse.js";
|
|
35
|
+
/** An identifier as Terraform's grammar allows it: letters, digits, `_`, `-`. */
|
|
36
|
+
const NAME = "[A-Za-z_][A-Za-z0-9_-]*";
|
|
37
|
+
const VAR_RE = new RegExp(`\\bvar\\.(${NAME})`, "g");
|
|
38
|
+
const LOCAL_RE = new RegExp(`\\blocal\\.(${NAME})`, "g");
|
|
39
|
+
const DATA_RE = new RegExp(`\\bdata\\.(${NAME})\\.(${NAME})`, "g");
|
|
40
|
+
const MODULE_RE = new RegExp(`\\bmodule\\.(${NAME})`, "g");
|
|
41
|
+
/** A provider reference once any `${...}` wrapper is off: `aws.west`. */
|
|
42
|
+
const PROVIDER_REF_RE = new RegExp(`^(${NAME})\\.(${NAME})$`);
|
|
43
|
+
/** hcl2json's wrapper around a bare expression, which a `provider` value always is. */
|
|
44
|
+
const INTERPOLATION_RE = /^\$\{(.*)\}$/s;
|
|
45
|
+
/**
|
|
46
|
+
* Every reference token in one string, in the vocabulary the index keys by:
|
|
47
|
+
* `var.region`, `local.tags`, `data.aws_ami.ubuntu`, `module.cdn`.
|
|
48
|
+
*
|
|
49
|
+
* `data.` is matched before `local.`/`var.` can see it, and a `module.cdn.url`
|
|
50
|
+
* yields `module.cdn`, since the call is what a reference makes used.
|
|
51
|
+
*/
|
|
52
|
+
export function collectReferences(text) {
|
|
53
|
+
const found = new Set();
|
|
54
|
+
for (const m of text.matchAll(DATA_RE))
|
|
55
|
+
found.add(`data.${m[1]}.${m[2]}`);
|
|
56
|
+
for (const m of text.matchAll(VAR_RE))
|
|
57
|
+
found.add(`var.${m[1]}`);
|
|
58
|
+
for (const m of text.matchAll(LOCAL_RE))
|
|
59
|
+
found.add(`local.${m[1]}`);
|
|
60
|
+
for (const m of text.matchAll(MODULE_RE))
|
|
61
|
+
found.add(`module.${m[1]}`);
|
|
62
|
+
return [...found];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The `provider`/`providers` meta-argument's own reference form, which is not
|
|
66
|
+
* an interpolation and has no prefix of its own: `provider = aws.west` names
|
|
67
|
+
* the `provider "aws" { alias = "west" }` block. Returned in the index's
|
|
68
|
+
* vocabulary, `provider.aws.west`, so it cannot collide with a resource
|
|
69
|
+
* address that happens to look the same.
|
|
70
|
+
*/
|
|
71
|
+
export function providerReference(value) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
const inner = INTERPOLATION_RE.exec(trimmed);
|
|
76
|
+
const m = PROVIDER_REF_RE.exec((inner ? inner[1] : trimmed).trim());
|
|
77
|
+
return m ? `provider.${m[1]}.${m[2]}` : undefined;
|
|
78
|
+
}
|
|
79
|
+
/** Every reference in one block body, provider meta-arguments included. */
|
|
80
|
+
export function referencesInBody(body) {
|
|
81
|
+
const found = new Set();
|
|
82
|
+
const visit = (value, key, depth) => {
|
|
83
|
+
if (depth > 6)
|
|
84
|
+
return;
|
|
85
|
+
if (typeof value === "string") {
|
|
86
|
+
for (const ref of collectReferences(value))
|
|
87
|
+
found.add(ref);
|
|
88
|
+
if (key === "provider") {
|
|
89
|
+
const provider = providerReference(value);
|
|
90
|
+
if (provider)
|
|
91
|
+
found.add(provider);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
for (const item of value)
|
|
97
|
+
visit(item, key, depth + 1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "object" && value !== null) {
|
|
101
|
+
for (const [k, v] of Object.entries(value)) {
|
|
102
|
+
// `providers = { aws = aws.west }` maps a child module's provider name
|
|
103
|
+
// to the caller's block, so every VALUE under `providers` is a
|
|
104
|
+
// provider reference, whatever its key.
|
|
105
|
+
visit(v, key === "providers" ? "provider" : k, depth + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
visit(body, "", 0);
|
|
110
|
+
return [...found];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the index over a whole build's entities. Scopes come from the entity
|
|
114
|
+
* keys, so a root and each of its descended child modules are separate, and
|
|
115
|
+
* two roots never pool.
|
|
116
|
+
*/
|
|
117
|
+
export function buildReferenceIndex(entities) {
|
|
118
|
+
const scopes = new Map();
|
|
119
|
+
for (const [key, entity] of entities) {
|
|
120
|
+
if (!isResourceDeclarable(entity))
|
|
121
|
+
continue;
|
|
122
|
+
const props = entity.props;
|
|
123
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {});
|
|
124
|
+
const scope = scopeOfKey(key);
|
|
125
|
+
let refs = scopes.get(scope);
|
|
126
|
+
if (!refs) {
|
|
127
|
+
refs = new Map();
|
|
128
|
+
scopes.set(scope, refs);
|
|
129
|
+
}
|
|
130
|
+
for (const ref of referencesInBody(body)) {
|
|
131
|
+
const from = refs.get(ref);
|
|
132
|
+
if (from)
|
|
133
|
+
from.add(key);
|
|
134
|
+
else
|
|
135
|
+
refs.set(ref, new Set([key]));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { scopes };
|
|
139
|
+
}
|
|
140
|
+
/** The entity keys in `scope` whose body references `token`. */
|
|
141
|
+
export function referencesTo(index, scope, token) {
|
|
142
|
+
return index.scopes.get(scope)?.get(token) ?? new Set();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Is `token` referenced anywhere in `scope` other than by `declaredBy` itself?
|
|
146
|
+
* The exclusion is what keeps a variable read only by its own `validation`
|
|
147
|
+
* block, or a local read only by its own `locals` block, from looking used.
|
|
148
|
+
*/
|
|
149
|
+
export function isReferenced(index, scope, token, declaredBy) {
|
|
150
|
+
const from = referencesTo(index, scope, token);
|
|
151
|
+
for (const key of from) {
|
|
152
|
+
if (key !== declaredBy)
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform build roots.
|
|
3
|
+
*
|
|
4
|
+
* An estate that keeps its `.tf` tree has no typed chant source for it. What
|
|
5
|
+
* it can declare is the directory: each entry in `terraform.roots` names a
|
|
6
|
+
* root module that parses at build time into entities, so the blocks are
|
|
7
|
+
* serialized into the build output and seen by the post-synth checks. Same
|
|
8
|
+
* shape `lexicons/k8s/src/kustomize/root.ts` uses for kustomize overlays: the
|
|
9
|
+
* render lives here, the plugin's `buildRoots()` member stays thin.
|
|
10
|
+
*
|
|
11
|
+
* Unlike the kustomize renderer, a missing directory or a `.tf` the parser
|
|
12
|
+
* refuses is a warning and zero entities for that root, never a throw. Reading
|
|
13
|
+
* someone else's estate is the whole job here, and half of it parsing is more
|
|
14
|
+
* useful than none of it, especially when the audit path (#2085) walks
|
|
15
|
+
* repositories it did not write.
|
|
16
|
+
*
|
|
17
|
+
* A root's own directory is not the end of the parse: a `module` block with a
|
|
18
|
+
* local source is followed into its directory and parsed as a child scope of
|
|
19
|
+
* the root (#2112). `./descend.ts` owns that walk and every refusal in it; the
|
|
20
|
+
* refusals surface here as this render's warnings.
|
|
21
|
+
*
|
|
22
|
+
* One pass runs after every root has parsed: `./edges.ts` resolves each
|
|
23
|
+
* block's `${...}` references into the entity keys they name, so the graph IR
|
|
24
|
+
* has edges to draw (#2265). It runs here rather than inside the per-file
|
|
25
|
+
* parse because an edge's target may be a block in a file, or a child module,
|
|
26
|
+
* that has not been read yet.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { isAbsolute, resolve } from "node:path";
|
|
30
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
31
|
+
import { LIVE_TYPE, parseTerraformRootDir } from "./parse.js";
|
|
32
|
+
import { descendModules, resolveCallModuleType } from "./descend.js";
|
|
33
|
+
import { resolveEntityReferences } from "./edges.js";
|
|
34
|
+
/**
|
|
35
|
+
* Parse each configured root into entities keyed `<root>/<address>`, plus its
|
|
36
|
+
* local child modules, keyed `<root>/module.<name>/<address>` (#2112). Roots
|
|
37
|
+
* are visited in declaration order, and one root's failure never stops the
|
|
38
|
+
* next.
|
|
39
|
+
*/
|
|
40
|
+
export async function renderTerraformRoots(opts) {
|
|
41
|
+
const entities = new Map();
|
|
42
|
+
const warnings = [];
|
|
43
|
+
// One verdict for the whole render: the mode is a project-level setting, so
|
|
44
|
+
// a refused `"all"` is said once rather than once per root (#2112).
|
|
45
|
+
const callModuleType = resolveCallModuleType(opts.callModuleType);
|
|
46
|
+
if (callModuleType.warning)
|
|
47
|
+
warnings.push(callModuleType.warning);
|
|
48
|
+
for (const [name, root] of Object.entries(opts.roots)) {
|
|
49
|
+
const dir = isAbsolute(root.dir) ? root.dir : resolve(opts.projectRoot, root.dir);
|
|
50
|
+
if (!existsSync(dir)) {
|
|
51
|
+
warnings.push(`terraform.roots.${name}: directory not found at ${dir}, no entities contributed`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const modeOptions = { binary: opts.binary, workspace: root.workspace, delete: root.delete, configEstate: root.estate };
|
|
56
|
+
const parsed = await parseTerraformRootDir(dir, name, opts.hcl2json, modeOptions);
|
|
57
|
+
for (const [key, entity] of parsed)
|
|
58
|
+
entities.set(key, entity);
|
|
59
|
+
// Local child modules, keyed `<root>/module.<name>/<address>` (#2112).
|
|
60
|
+
// Their refusals are warnings on the root that called them, since the
|
|
61
|
+
// call site is where a reader can act on one.
|
|
62
|
+
const descended = await descendModules(parsed, {
|
|
63
|
+
dir,
|
|
64
|
+
root: name,
|
|
65
|
+
projectRoot: opts.projectRoot,
|
|
66
|
+
...(opts.moduleRoot !== undefined ? { moduleRoot: isAbsolute(opts.moduleRoot) ? opts.moduleRoot : resolve(opts.projectRoot, opts.moduleRoot) } : {}),
|
|
67
|
+
callModuleType: callModuleType.effective,
|
|
68
|
+
hcl2json: opts.hcl2json,
|
|
69
|
+
modeOptions,
|
|
70
|
+
});
|
|
71
|
+
for (const [key, entity] of descended.entities)
|
|
72
|
+
entities.set(key, entity);
|
|
73
|
+
warnings.push(...descended.warnings);
|
|
74
|
+
// A `live` block or `estate.chdf.hcl` sidecar only takes effect under
|
|
75
|
+
// choudoufu; under any other binary it parses fine but is inert, which
|
|
76
|
+
// is worth a warning rather than silence (#2103).
|
|
77
|
+
for (const entity of parsed.values()) {
|
|
78
|
+
if (entity.entityType !== LIVE_TYPE || !isResourceDeclarable(entity))
|
|
79
|
+
continue;
|
|
80
|
+
if (entity.props.mode === "state") {
|
|
81
|
+
warnings.push(`terraform.roots.${name}: declares a live estate (a \`live\` block or ${JSON.stringify("estate.chdf.hcl")} sidecar), but terraform.binary is ${JSON.stringify(opts.binary ?? "terraform")}, not "choudoufu", so the live declaration is inert.`);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
warnings.push(`terraform.roots.${name}: could not parse ${dir}, ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Every root and every descended child module is parsed by now, which is
|
|
92
|
+
// what reference resolution needs: an edge points at an entity key, and the
|
|
93
|
+
// keys only all exist once the last descent has run (#2265). One pass over
|
|
94
|
+
// the whole map, one expression cache, resolution still bounded to each
|
|
95
|
+
// block's own module scope.
|
|
96
|
+
await resolveEntityReferences(entities, opts.hcl2json);
|
|
97
|
+
return { entities, warnings };
|
|
98
|
+
}
|