@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.
Files changed (141) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.js +111 -0
  2. package/dist/__fixtures__/terragrunt/record.js +57 -0
  3. package/dist/behaviour/index.js +6 -0
  4. package/dist/behaviour/kinds.js +194 -0
  5. package/dist/behaviour/predict.js +297 -0
  6. package/dist/behaviour/reference-catalog.js +178 -0
  7. package/dist/behaviour/request.js +482 -0
  8. package/dist/change-set.js +234 -0
  9. package/dist/changed-roots.js +97 -0
  10. package/dist/codegen/docs-cli.js +3 -0
  11. package/dist/codegen/docs.js +44 -0
  12. package/dist/codegen/generate-cli.js +9 -0
  13. package/dist/codegen/generate.js +59 -0
  14. package/dist/codegen/package.js +43 -0
  15. package/dist/commands.js +106 -0
  16. package/dist/components/capability-plugin.d.ts +38 -2
  17. package/dist/components/capability-plugin.d.ts.map +1 -1
  18. package/dist/components/capability-plugin.js +260 -0
  19. package/dist/composites/terraform-adopt-op.js +152 -0
  20. package/dist/composites/terraform-apply-op.js +183 -0
  21. package/dist/composites/terraform-pin-rollout-op.js +45 -0
  22. package/dist/composites/terraform-watch-op.js +219 -0
  23. package/dist/config.js +118 -0
  24. package/dist/describe-resources.js +984 -0
  25. package/dist/generated/index.js +1 -0
  26. package/dist/hcl/descend.js +208 -0
  27. package/dist/hcl/edges.js +287 -0
  28. package/dist/hcl/jsonencode.js +442 -0
  29. package/dist/hcl/marker.js +212 -0
  30. package/dist/hcl/parse.js +372 -0
  31. package/dist/hcl/references.js +156 -0
  32. package/dist/hcl/roots.js +98 -0
  33. package/dist/hcl/suppressions.js +189 -0
  34. package/dist/hcl/value.js +72 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +46 -0
  38. package/dist/integrity.json +2 -2
  39. package/dist/lint/audit-catalog.js +501 -0
  40. package/dist/lint/audit-lineage.js +442 -0
  41. package/dist/lint/post-synth/blocks.js +87 -0
  42. package/dist/lint/post-synth/fixtures/load.js +94 -0
  43. package/dist/lint/post-synth/flat-attributes.js +135 -0
  44. package/dist/lint/post-synth/index.js +90 -0
  45. package/dist/lint/post-synth/module-source.js +153 -0
  46. package/dist/lint/post-synth/scope.js +104 -0
  47. package/dist/lint/post-synth/terragrunt.js +63 -0
  48. package/dist/lint/post-synth/tf001.js +138 -0
  49. package/dist/lint/post-synth/tf002.js +143 -0
  50. package/dist/lint/post-synth/tf003.js +60 -0
  51. package/dist/lint/post-synth/tf004.js +54 -0
  52. package/dist/lint/post-synth/tf005.js +68 -0
  53. package/dist/lint/post-synth/tf006.js +43 -0
  54. package/dist/lint/post-synth/tf007.js +66 -0
  55. package/dist/lint/post-synth/tf008.js +64 -0
  56. package/dist/lint/post-synth/tf009.js +49 -0
  57. package/dist/lint/post-synth/tf010.js +40 -0
  58. package/dist/lint/post-synth/tf011.js +38 -0
  59. package/dist/lint/post-synth/tf012.js +36 -0
  60. package/dist/lint/post-synth/tf013.js +53 -0
  61. package/dist/lint/post-synth/tf014.js +61 -0
  62. package/dist/lint/post-synth/tf015.js +62 -0
  63. package/dist/lint/post-synth/tf016.js +66 -0
  64. package/dist/lint/post-synth/tf017.js +39 -0
  65. package/dist/lint/post-synth/tf018.js +63 -0
  66. package/dist/lint/post-synth/tf019.js +60 -0
  67. package/dist/lint/post-synth/tf020.js +103 -0
  68. package/dist/lint/post-synth/tf021.js +117 -0
  69. package/dist/lint/post-synth/tf022.js +51 -0
  70. package/dist/lint/post-synth/tf024.js +62 -0
  71. package/dist/lint/post-synth/tf025.js +83 -0
  72. package/dist/lint/post-synth/tf026.js +74 -0
  73. package/dist/lint/post-synth/tf027.js +66 -0
  74. package/dist/lint/post-synth/tf028.js +83 -0
  75. package/dist/lint/post-synth/tf029.js +73 -0
  76. package/dist/lint/post-synth/tf030.js +253 -0
  77. package/dist/lint/post-synth/tf031.js +332 -0
  78. package/dist/lint/post-synth/tf032.js +201 -0
  79. package/dist/lint/post-synth/tf033.js +126 -0
  80. package/dist/lint/post-synth/tf034.js +45 -0
  81. package/dist/lint/post-synth/tf035.js +63 -0
  82. package/dist/lint/post-synth/tf036.js +97 -0
  83. package/dist/lint/post-synth/tf037.js +80 -0
  84. package/dist/lint/post-synth/tf038.js +63 -0
  85. package/dist/lint/post-synth/tf039.js +55 -0
  86. package/dist/lint/post-synth/tf040.js +67 -0
  87. package/dist/lint/post-synth/tf041.js +53 -0
  88. package/dist/lint/post-synth/tf042.js +38 -0
  89. package/dist/lint/post-synth/tf043.js +45 -0
  90. package/dist/lint/post-synth/tf044.js +45 -0
  91. package/dist/lint/post-synth/tf045.js +44 -0
  92. package/dist/lint/rules/fixtures/load.js +50 -0
  93. package/dist/lint/rules/index.js +4 -0
  94. package/dist/lint/rules/plan-before-apply.js +136 -0
  95. package/dist/lint/secret-shape.js +149 -0
  96. package/dist/live-ownership.js +57 -0
  97. package/dist/lsp/completions.js +95 -0
  98. package/dist/lsp/context.js +358 -0
  99. package/dist/lsp/hover.js +45 -0
  100. package/dist/lsp/option-keys.js +93 -0
  101. package/dist/manifest.json +1 -1
  102. package/dist/op/activities/index.js +30 -0
  103. package/dist/op/activities/live-detect.js +143 -0
  104. package/dist/op/activities/pin-rollout.js +45 -0
  105. package/dist/op/activities/terraform.js +1270 -0
  106. package/dist/op/activity-contracts.js +162 -0
  107. package/dist/op/adoption.js +272 -0
  108. package/dist/op/builders.js +125 -0
  109. package/dist/op/resolve-root-mode.js +77 -0
  110. package/dist/package-cli.js +16 -0
  111. package/dist/pin/edit-ts.js +76 -0
  112. package/dist/pin/edit.js +118 -0
  113. package/dist/pin/forge.js +65 -0
  114. package/dist/pin/index.js +16 -0
  115. package/dist/pin/rollout.d.ts.map +1 -1
  116. package/dist/pin/rollout.js +382 -0
  117. package/dist/pin/scan.js +287 -0
  118. package/dist/pin/source.js +171 -0
  119. package/dist/pin/waves.js +109 -0
  120. package/dist/plan-digest.js +67 -0
  121. package/dist/plugin.js +323 -0
  122. package/dist/serializer.js +23 -0
  123. package/dist/state-ownership.js +29 -0
  124. package/dist/terragrunt/affected.d.ts.map +1 -1
  125. package/dist/terragrunt/affected.js +290 -0
  126. package/dist/terragrunt/index.js +10 -0
  127. package/dist/terragrunt/mocks.js +270 -0
  128. package/dist/terragrunt/run.js +264 -0
  129. package/dist/terragrunt/units.js +195 -0
  130. package/dist/terragrunt/wave.js +125 -0
  131. package/dist/validate-cli.js +4 -0
  132. package/dist/validate.js +31 -0
  133. package/package.json +10 -10
  134. package/src/codegen/package.ts +2 -2
  135. package/src/components/capability-plugin.test.ts +97 -2
  136. package/src/components/capability-plugin.ts +117 -12
  137. package/src/components/pr-loop.tofu.test.ts +51 -11
  138. package/src/index.ts +1 -0
  139. package/src/pin/rollout.ts +2 -1
  140. package/src/terragrunt/affected.test.ts +11 -0
  141. package/src/terragrunt/affected.ts +2 -1
@@ -0,0 +1,297 @@
1
+ /**
2
+ * The live path for choudoufu roots (#2360): a prediction of the account as
3
+ * it stands, drift included.
4
+ *
5
+ * `describeResources` (`../describe-resources.ts`) answers "does this declared
6
+ * block exist" for a live root by running `choudoufu live-plan -json` through
7
+ * the applier's own activity. This does the same two reads — `live-ls -json`
8
+ * for what the account holds, `live-plan -json` for how that binds to the
9
+ * declaration — through the same injected activities, builds the engine's
10
+ * request from them (`./request.ts`), and hands it to whichever lexicon fronts
11
+ * the engine. The terraform lexicon has no engine of its own and dials none:
12
+ * `deps.predict` is core's `predictBehaviour` in production, and a fixture
13
+ * in a test.
14
+ *
15
+ * ## Four moves, in this order
16
+ *
17
+ * 1. **Read.** One `live-ls` and one `live-plan` per live root — none at all
18
+ * under `from: "declared"` — no ambient
19
+ * credentials and no new shell-out: the activities are the ones
20
+ * `describeResources` already runs. A root whose read fails reports every
21
+ * resource it declares `read-failed`, naming the root, and is not sent — a
22
+ * failed read must never render as an estate with nothing in it.
23
+ * 2. **Build.** `terraformBehaviourRequest`, pure, over the parsed documents.
24
+ * 3. **Screen.** `screenBehaviourRequest`, before any engine is asked. Live
25
+ * props are exactly where a resolved connection string or a secret written
26
+ * into a tag can appear, and the screen's key-name, value-shape,
27
+ * URL-userinfo and walk-depth rules all apply to them. A refusal here is
28
+ * the result.
29
+ * 4. **Ask, and account for the rest.** The engine-fronting lexicon answers
30
+ * for what was sent. What was not sent — a resource the plan could not
31
+ * read, one withheld by `owned`, every resource of a root whose read
32
+ * failed — is merged into the report as `unpredicted` with its reason, so
33
+ * every declared name still lands in one map or the other.
34
+ *
35
+ * A root that is not live is built from its declaration by the same producer,
36
+ * so a project mixing stock and live roots gets one request with both, and
37
+ * `sources` says which is which. With no live root at all this is the
38
+ * declared path, containment included.
39
+ *
40
+ * ## Both halves of the delta come out of here
41
+ *
42
+ * `from: "declared"` reads no account and builds every root from its file,
43
+ * including a root that is live. That is the other side of the delta the epic
44
+ * wants, and it is deliberately the same function rather than a second one:
45
+ * two reports that differ because one producer was handed different input are
46
+ * a statement about the estate, and two reports that differ because two
47
+ * producers disagree about how to assemble a graph are a statement about
48
+ * chant. `edgeCoverage.containmentEdges` is the field that made the
49
+ * difference concrete (#2360's third review comment) — it is built by the same
50
+ * catalog over the same reconstruction on both sides here, so a "one zone
51
+ * lost" verdict that differs between the two differs because the account
52
+ * differs.
53
+ */
54
+ import { behaviourReport, isBehaviourRefusalReport, screenBehaviourRequest, } from "@intentius/chant/behaviour";
55
+ import { createBehaviourPredict } from "@intentius/chant/behaviour-predict";
56
+ import { indexLivePlan, readLiveLs } from "../describe-resources.js";
57
+ import { RESOURCE_TYPE } from "../hcl/parse.js";
58
+ import { choudoufuLiveLs, choudoufuLivePlanDocument } from "../op/activities/terraform.js";
59
+ import { terraformBehaviourKinds } from "./kinds.js";
60
+ import { terraformBehaviourRequest, } from "./request.js";
61
+ /** The name this path screens and refuses under. */
62
+ export const TERRAFORM = "terraform";
63
+ /**
64
+ * What the plugin method runs with, where a caller injects nothing (#2495).
65
+ *
66
+ * The two reads are the activities `describeResources` already drives. The
67
+ * engine front is core's, handed this lexicon's coverage rows and no other's:
68
+ * a plugin method takes options alone and cannot see which other lexicons the
69
+ * project configured, and every name this producer sends is a terraform block
70
+ * or a name it passed through untouched. A passed-through name of another
71
+ * lexicon's type comes back `unknown-type`, which is true of what this path
72
+ * knows about it.
73
+ */
74
+ const REAL_DEPS = {
75
+ liveLs: choudoufuLiveLs,
76
+ livePlan: choudoufuLivePlanDocument,
77
+ predict: createBehaviourPredict({ kinds: [terraformBehaviourKinds] }),
78
+ };
79
+ /**
80
+ * The build's own entities, read again from the project's config: what
81
+ * `buildRoots()` renders, with `mode`, `body` and `references` as the parse
82
+ * stamps them. The config is found the way `observeAmbient` and the
83
+ * activities find it, so `terraform.roots` means one thing to all of them.
84
+ */
85
+ export const readTerraformDeclaration = async (cwd) => {
86
+ const { loadChantConfigUpward } = await import("@intentius/chant/config");
87
+ const { renderTerraformRoots } = await import("../hcl/roots.js");
88
+ const { dirname, resolve } = await import("node:path");
89
+ const start = resolve(cwd ?? process.cwd());
90
+ const { config, configPath } = await loadChantConfigUpward(start);
91
+ const namespace = config.terraform;
92
+ const entities = new Map();
93
+ if (!namespace?.roots || Object.keys(namespace.roots).length === 0)
94
+ return entities;
95
+ const rendered = await renderTerraformRoots({
96
+ projectRoot: configPath ? dirname(configPath) : start,
97
+ roots: namespace.roots,
98
+ binary: namespace.binary,
99
+ callModuleType: namespace.callModuleType,
100
+ moduleRoot: namespace.moduleRoot,
101
+ });
102
+ for (const [name, entity] of rendered.entities) {
103
+ const e = entity;
104
+ entities.set(name, {
105
+ entityType: e.entityType,
106
+ props: e.props ?? {},
107
+ ...(Array.isArray(e.references) ? { references: e.references } : {}),
108
+ });
109
+ }
110
+ return entities;
111
+ };
112
+ const rootOf = (entity) => typeof entity?.props.root === "string" ? entity.props.root : undefined;
113
+ /**
114
+ * Read the contract's options as this producer's (#2495): the plugin method's
115
+ * whole adaptation. Two things are missing from an entity by the time a graph
116
+ * has carried it here, and which one depends on the graph.
117
+ *
118
+ * ## A declared graph has lost its `references`
119
+ *
120
+ * `root` and `mode` need no work: they are on `props`, the graph projects
121
+ * `props` onto a node's `attrs` as they stand, and `behaviourRequestFromIr`
122
+ * hands `attrs` back as `props`. `references` are left off `attrs` on
123
+ * purpose, because an edge already says the same thing (`graph-ir.ts`'s
124
+ * `SKIP_KEYS`), and the contract's `edges` arrive in their place. An `IREdge`
125
+ * is an `EntityReference` plus the entity it leaves from, so grouping the
126
+ * edges by `from` gives every entity its references back, field for field.
127
+ * Without this a block whose body holds a `${…}` is counted as an unresolved
128
+ * kind on a graph that resolved it. An entity that still carries its own
129
+ * `references` keeps them: the edges are the copy.
130
+ *
131
+ * ## A live graph has lost the declaration
132
+ *
133
+ * `chant graph --live` hands over observed nodes: an address, a root, the
134
+ * provider's type, and no `mode`, no `body` and no edge. Read as they stand,
135
+ * no root is live, so the account is never read here; no block has a size,
136
+ * because `body.instance_type` went with the body; and containment is empty.
137
+ * The figures that come back are well-formed and differ from the declared
138
+ * side's for reasons that have nothing to do with the account, which is the
139
+ * delta this producer exists to keep honest.
140
+ *
141
+ * So under `from: "live"`, a root any of whose entities arrived with no
142
+ * `mode` is declared again from the project's own config, and the producer
143
+ * then reads the account itself, as it does for any other caller. The
144
+ * observed entities of that root are set aside, the undeclared ones
145
+ * included: an owned orphan is the live read's to list, under the key
146
+ * `observeAmbient` gave it, and one the account no longer holds is not in a
147
+ * prediction of the account.
148
+ *
149
+ * A root the config does not render is left as it arrived. That is a worse
150
+ * reading and the only one available.
151
+ *
152
+ * ## What stops here
153
+ *
154
+ * The caller's `edges` and `edgeCoverage`. Both are this producer's to
155
+ * compute, from whichever reads it ends up making (see
156
+ * {@link TerraformPredictOptions}).
157
+ */
158
+ export async function terraformPredictOptionsFrom(options, redeclare = readTerraformDeclaration) {
159
+ const { entities: given, edges, edgeCoverage: _edgeCoverage, ...rest } = options;
160
+ const leaving = new Map();
161
+ for (const edge of edges ?? []) {
162
+ if (edge.kind !== "ref")
163
+ continue;
164
+ const reference = {
165
+ to: edge.to,
166
+ ...(edge.viaAttr !== undefined ? { viaAttr: edge.viaAttr } : {}),
167
+ ...(edge.toAttr !== undefined ? { toAttr: edge.toAttr } : {}),
168
+ };
169
+ (leaving.get(edge.from) ?? leaving.set(edge.from, []).get(edge.from)).push(reference);
170
+ }
171
+ const entities = new Map();
172
+ for (const [name, entity] of given) {
173
+ const own = entity.references;
174
+ const references = Array.isArray(own) ? own : leaving.get(name);
175
+ entities.set(name, references === undefined ? entity : { ...entity, references });
176
+ }
177
+ if (options.from !== "live")
178
+ return { ...rest, entities };
179
+ const observed = new Set();
180
+ for (const name of options.entityNames) {
181
+ const entity = entities.get(name);
182
+ const root = rootOf(entity);
183
+ if (root !== undefined && entity?.props.mode === undefined)
184
+ observed.add(root);
185
+ }
186
+ if (observed.size === 0)
187
+ return { ...rest, entities };
188
+ const declared = new Map();
189
+ for (const [name, entity] of await redeclare()) {
190
+ const root = rootOf(entity);
191
+ if (root === undefined || !observed.has(root))
192
+ continue;
193
+ (declared.get(root) ?? declared.set(root, new Map()).get(root)).set(name, entity);
194
+ }
195
+ const entityNames = options.entityNames.filter((name) => {
196
+ const root = rootOf(entities.get(name));
197
+ if (root === undefined || !declared.has(root))
198
+ return true;
199
+ entities.delete(name);
200
+ return false;
201
+ });
202
+ for (const again of declared.values()) {
203
+ for (const [name, entity] of again) {
204
+ entities.set(name, entity);
205
+ entityNames.push(name);
206
+ }
207
+ }
208
+ return { ...rest, entityNames, entities };
209
+ }
210
+ /** Which roots the request would read live: those whose entities the parse stamped `mode: "live"`. */
211
+ export function liveRootsOf(entityNames, entities) {
212
+ const roots = new Set();
213
+ for (const name of entityNames) {
214
+ const props = entities.get(name)?.props;
215
+ const root = typeof props?.root === "string" ? props.root : undefined;
216
+ if (root && props?.mode === "live")
217
+ roots.add(root);
218
+ }
219
+ return [...roots].sort();
220
+ }
221
+ function readsAsCredentials(text) {
222
+ return /\b(no valid credential|credentials?|not authorized|unauthorized|accessdenied|access denied|expiredtoken|invalidclienttokenid|signature)\b/i.test(text);
223
+ }
224
+ /**
225
+ * Predict from the account for every live root, and from the declaration for
226
+ * every other, through the engine `deps.predict` fronts.
227
+ */
228
+ export async function predictTerraformBehaviour(options, deps = REAL_DEPS) {
229
+ const { cwd, from: _from, ...contract } = options;
230
+ const where = cwd ? { cwd } : {};
231
+ const reads = [];
232
+ const failed = new Map();
233
+ const roots = options.from === "declared" ? [] : liveRootsOf(options.entityNames, options.entities);
234
+ for (const root of roots) {
235
+ try {
236
+ const listed = await deps.liveLs({ root, consistent: true, ...where });
237
+ const planned = await deps.livePlan({ root, ...where });
238
+ reads.push({ root, listing: readLiveLs(listed.json), plan: indexLivePlan(planned.json) });
239
+ }
240
+ catch (err) {
241
+ const message = err instanceof Error ? err.message : String(err);
242
+ failed.set(root, message);
243
+ }
244
+ }
245
+ // A root whose read failed contributes no node and every one of its
246
+ // resources as `read-failed`, naming the root: the tri-state's rule for a
247
+ // broken read, applied to a prediction.
248
+ const unpredicted = {};
249
+ const entityNames = options.entityNames.filter((name) => {
250
+ const entity = options.entities.get(name);
251
+ const root = typeof entity?.props.root === "string" ? entity.props.root : undefined;
252
+ if (!root || !failed.has(root))
253
+ return true;
254
+ if (entity?.entityType === RESOURCE_TYPE) {
255
+ const message = failed.get(root);
256
+ unpredicted[name] = {
257
+ type: RESOURCE_TYPE,
258
+ reason: "read-failed",
259
+ detail: `terraform.roots.${root}: the live read failed, so the account was not predicted for this root` +
260
+ `${readsAsCredentials(message) ? " (the failure names credentials)" : ""}: ${message.split("\n")[0]}`,
261
+ };
262
+ return false;
263
+ }
264
+ return true;
265
+ });
266
+ const built = terraformBehaviourRequest({
267
+ environment: contract.environment,
268
+ buildOutput: contract.buildOutput,
269
+ traffic: contract.traffic,
270
+ from: options.from,
271
+ ...(contract.region !== undefined ? { region: contract.region } : {}),
272
+ ...(contract.stack !== undefined ? { stack: contract.stack } : {}),
273
+ ...(contract.owned !== undefined ? { owned: contract.owned } : {}),
274
+ entityNames,
275
+ entities: options.entities,
276
+ live: reads,
277
+ });
278
+ Object.assign(unpredicted, built.unpredicted);
279
+ const unsafe = screenBehaviourRequest(TERRAFORM, built.request);
280
+ if (unsafe)
281
+ return unsafe;
282
+ const result = await deps.predict(built.request);
283
+ if (isBehaviourRefusalReport(result))
284
+ return result;
285
+ const extra = Object.keys(unpredicted);
286
+ if (extra.length === 0)
287
+ return result;
288
+ return behaviourReport({
289
+ entityNames: [...built.request.entityNames, ...extra],
290
+ traffic: result.meta.at.traffic,
291
+ edgeCoverage: result.meta.edgeCoverage,
292
+ }, {
293
+ engine: result.meta.engine,
294
+ version: result.meta.version,
295
+ ...(result.meta.total ? { total: result.meta.total } : {}),
296
+ }, result.entities, { ...result.unpredicted, ...unpredicted });
297
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * How the aws provider's resources reference each other, as a `ReferenceCatalog`
3
+ * over terraform's own argument names (#2360).
4
+ *
5
+ * `packages/core/src/graph-refs.ts`'s `reconstructEdges` turns a bag of nodes
6
+ * into a graph by reading identifier values out of each node's attributes and
7
+ * matching them against every other node's identity. The aws lexicon's
8
+ * catalog does that over the SDK's shape (`VpcId`, `SecurityGroups[].GroupId`);
9
+ * this one does it over a terraform block's shape (`vpc_id`,
10
+ * `vpc_security_group_ids[]`), because that is the shape a block's body has
11
+ * after `../hcl/parse.ts` and the shape `./request.ts` hands the resolver on
12
+ * both the declared and the live side.
13
+ *
14
+ * ## Keyed by provider type, not by chant entity type
15
+ *
16
+ * Every terraform `resource` block is `Terraform::Resource` to chant. A rule
17
+ * keyed on that would apply to a VPC and an instance alike, so the nodes
18
+ * `./request.ts` builds for the resolver carry the **provider type** as their
19
+ * `kind` — `aws_subnet` — and the rules here are keyed the same way. That is
20
+ * also why this catalog is not declared on the plugin as `referenceCatalog`:
21
+ * `chant graph --live` builds its nodes with `kind: "Terraform::Resource"`
22
+ * and attributes that carry no arguments at all, so declaring it there would
23
+ * be a catalog that matched nothing and looked like coverage.
24
+ *
25
+ * ## Two relations, and why the split matters to an engine
26
+ *
27
+ * A `containment` rule (subnet ∈ VPC, instance ∈ subnet) becomes a boundary
28
+ * pair and a traversable containment edge; a `reference` rule becomes an
29
+ * ordinary edge. `PredictBehaviourOptions.edges` carries the second and
30
+ * `edgeCoverage.containmentEdges` the first, and the contract keeps them
31
+ * apart for a reason that is the whole of #2360's first review comment: a
32
+ * "one zone lost" verdict is computed over zone membership, and membership is
33
+ * not a reference. Both sides of the declared-versus-live delta run these
34
+ * same rules, so neither side is structurally short of containment the other
35
+ * has.
36
+ *
37
+ * ## Multi-kind arguments carry no `targetKind`
38
+ *
39
+ * `reconstructEdges` records a value under a rule with a `targetKind` as
40
+ * dangling when the value resolves to a node of another kind. An argument
41
+ * that legitimately points at more than one kind — a target-group attachment's
42
+ * `target_id` is an instance or a function — would therefore report every
43
+ * match of the other kind as a reference leaving the estate. Those rules name
44
+ * no `targetKind` and take whatever the identifier resolves to, the same
45
+ * choice the aws catalog makes for its CloudFormation template paths.
46
+ *
47
+ * ## What "no rule" means
48
+ *
49
+ * A kind with no rule here produces no edges and, on its own, no complaint —
50
+ * the quiet gap `edgeCoverage.unresolvedKinds` exists to name. But "no rule"
51
+ * has two causes that should not read alike: nobody has written one, or the
52
+ * kind holds no identifier-bearing argument to write one about. A VPC
53
+ * references nothing; an SQS queue names its dead-letter queue inside a JSON
54
+ * document this catalog does not read. {@link REFERENCES_NOTHING} is the list
55
+ * of kinds in the first position, checked rather than assumed, so that an
56
+ * estate with a VPC in it is not `partial` forever and an estate with a queue
57
+ * in it is not `complete` by omission.
58
+ */
59
+ /**
60
+ * Provider types this catalog has looked at and found to hold no argument
61
+ * that identifies another resource. A node of one of these kinds is not an
62
+ * unresolved kind: nothing was looked for because there is nothing to find.
63
+ *
64
+ * Deliberately short. A kind is added here when every identifier-bearing
65
+ * argument it has is also a rule below, or when it has none — not when its
66
+ * references happen to be inconvenient to read.
67
+ */
68
+ export const REFERENCES_NOTHING = new Set([
69
+ "aws_vpc",
70
+ "aws_s3_bucket",
71
+ "aws_ecs_cluster",
72
+ "aws_ecr_repository",
73
+ "aws_route53_zone",
74
+ ]);
75
+ /** The catalog. Rules are grouped by the relation they produce. */
76
+ export const TERRAFORM_REFERENCE_CATALOG = {
77
+ // Every node is indexed by its own id and physical id (`reconstructEdges`
78
+ // does that itself), and a block's body never carries a second identifier
79
+ // for the block, so there are no identity rules to declare.
80
+ identities: [],
81
+ refs: [
82
+ // ── containment (→ boundary pairs, and containment edges) ──
83
+ { from: "aws_subnet", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
84
+ { from: "aws_security_group", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
85
+ { from: "aws_route_table", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
86
+ { from: "aws_internet_gateway", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
87
+ { from: "aws_lb_target_group", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
88
+ { from: "aws_vpc_endpoint", path: "vpc_id", targetKind: "aws_vpc", relation: "containment", label: "in VPC" },
89
+ // An instance's subnet is the hop a topology fold takes first, so it is
90
+ // traversable as an ordinary edge too — the same `viaAttr` opt-in the aws
91
+ // catalog gives `AWS::EC2::Instance.SubnetId`.
92
+ { from: "aws_instance", path: "subnet_id", targetKind: "aws_subnet", relation: "containment", label: "in subnet", viaAttr: "subnet_id" },
93
+ { from: "aws_nat_gateway", path: "subnet_id", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
94
+ { from: "aws_network_interface", path: "subnet_id", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
95
+ { from: "aws_lb", path: "subnets[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
96
+ { from: "aws_alb", path: "subnets[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
97
+ { from: "aws_lambda_function", path: "vpc_config[].subnet_ids[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
98
+ { from: "aws_db_subnet_group", path: "subnet_ids[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
99
+ { from: "aws_elasticache_subnet_group", path: "subnet_ids[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
100
+ { from: "aws_db_instance", path: "db_subnet_group_name", targetKind: "aws_db_subnet_group", relation: "containment", label: "in subnet group" },
101
+ { from: "aws_rds_cluster", path: "db_subnet_group_name", targetKind: "aws_db_subnet_group", relation: "containment", label: "in subnet group" },
102
+ { from: "aws_elasticache_cluster", path: "subnet_group_name", targetKind: "aws_elasticache_subnet_group", relation: "containment", label: "in subnet group" },
103
+ { from: "aws_ecs_service", path: "network_configuration[].subnets[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
104
+ { from: "aws_eks_cluster", path: "vpc_config[].subnet_ids[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
105
+ { from: "aws_eks_node_group", path: "subnet_ids[]", targetKind: "aws_subnet", relation: "containment", label: "in subnet" },
106
+ // ── references (→ edges) ──
107
+ { from: "aws_security_group_rule", path: "security_group_id", targetKind: "aws_security_group", relation: "reference", label: "rule on", viaAttr: "security_group_id" },
108
+ { from: "aws_security_group_rule", path: "source_security_group_id", targetKind: "aws_security_group", relation: "reference", label: "allows" },
109
+ { from: "aws_vpc_security_group_ingress_rule", path: "security_group_id", targetKind: "aws_security_group", relation: "reference", label: "rule on", viaAttr: "security_group_id" },
110
+ { from: "aws_vpc_security_group_ingress_rule", path: "referenced_security_group_id", targetKind: "aws_security_group", relation: "reference", label: "allows" },
111
+ { from: "aws_vpc_security_group_egress_rule", path: "security_group_id", targetKind: "aws_security_group", relation: "reference", label: "rule on", viaAttr: "security_group_id" },
112
+ { from: "aws_vpc_security_group_egress_rule", path: "referenced_security_group_id", targetKind: "aws_security_group", relation: "reference", label: "allows" },
113
+ { from: "aws_instance", path: "vpc_security_group_ids[]", targetKind: "aws_security_group", relation: "reference", label: "sg", viaAttr: "vpc_security_group_ids" },
114
+ { from: "aws_instance", path: "security_groups[]", targetKind: "aws_security_group", relation: "reference", label: "sg", viaAttr: "security_groups" },
115
+ { from: "aws_instance", path: "iam_instance_profile", targetKind: "aws_iam_instance_profile", relation: "reference", label: "as" },
116
+ { from: "aws_network_interface", path: "security_groups[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
117
+ { from: "aws_lambda_function", path: "vpc_config[].security_group_ids[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
118
+ { from: "aws_lambda_function", path: "role", targetKind: "aws_iam_role", relation: "reference", label: "as" },
119
+ { from: "aws_db_instance", path: "vpc_security_group_ids[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
120
+ { from: "aws_rds_cluster", path: "vpc_security_group_ids[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
121
+ { from: "aws_rds_cluster_instance", path: "cluster_identifier", targetKind: "aws_rds_cluster", relation: "reference", label: "in cluster" },
122
+ { from: "aws_elasticache_cluster", path: "security_group_ids[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
123
+ { from: "aws_lb", path: "security_groups[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
124
+ { from: "aws_alb", path: "security_groups[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
125
+ { from: "aws_lb_listener", path: "load_balancer_arn", targetKind: "aws_lb", relation: "reference", label: "on" },
126
+ { from: "aws_lb_listener", path: "default_action[].target_group_arn", targetKind: "aws_lb_target_group", relation: "reference", label: "forwards to" },
127
+ { from: "aws_lb_target_group_attachment", path: "target_group_arn", targetKind: "aws_lb_target_group", relation: "reference", label: "in group" },
128
+ // An instance, a function or an IP; no `targetKind` (see the module doc).
129
+ { from: "aws_lb_target_group_attachment", path: "target_id", relation: "reference", label: "targets" },
130
+ { from: "aws_route", path: "route_table_id", targetKind: "aws_route_table", relation: "reference", label: "in table", viaAttr: "route_table_id" },
131
+ { from: "aws_route", path: "gateway_id", targetKind: "aws_internet_gateway", relation: "reference", label: "via", viaAttr: "gateway_id" },
132
+ { from: "aws_route", path: "nat_gateway_id", targetKind: "aws_nat_gateway", relation: "reference", label: "via" },
133
+ { from: "aws_route_table_association", path: "subnet_id", targetKind: "aws_subnet", relation: "reference", label: "associates", viaAttr: "subnet_id" },
134
+ { from: "aws_route_table_association", path: "route_table_id", targetKind: "aws_route_table", relation: "reference", label: "to table", viaAttr: "route_table_id" },
135
+ { from: "aws_nat_gateway", path: "allocation_id", targetKind: "aws_eip", relation: "reference", label: "uses" },
136
+ { from: "aws_eip", path: "instance", targetKind: "aws_instance", relation: "reference", label: "attached to" },
137
+ { from: "aws_eip", path: "network_interface", targetKind: "aws_network_interface", relation: "reference", label: "attached to" },
138
+ { from: "aws_ecs_service", path: "cluster", targetKind: "aws_ecs_cluster", relation: "reference", label: "in cluster" },
139
+ { from: "aws_ecs_service", path: "task_definition", targetKind: "aws_ecs_task_definition", relation: "reference", label: "runs" },
140
+ { from: "aws_ecs_service", path: "load_balancer[].target_group_arn", targetKind: "aws_lb_target_group", relation: "reference", label: "registered in" },
141
+ { from: "aws_ecs_service", path: "network_configuration[].security_groups[]", targetKind: "aws_security_group", relation: "reference", label: "sg" },
142
+ { from: "aws_ecs_task_definition", path: "execution_role_arn", targetKind: "aws_iam_role", relation: "reference", label: "as" },
143
+ { from: "aws_ecs_task_definition", path: "task_role_arn", targetKind: "aws_iam_role", relation: "reference", label: "as" },
144
+ { from: "aws_eks_node_group", path: "cluster_name", targetKind: "aws_eks_cluster", relation: "reference", label: "in cluster" },
145
+ { from: "aws_eks_cluster", path: "role_arn", targetKind: "aws_iam_role", relation: "reference", label: "as" },
146
+ { from: "aws_s3_bucket_policy", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "grant on" },
147
+ { from: "aws_s3_bucket_versioning", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "setting on" },
148
+ { from: "aws_s3_bucket_public_access_block", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "setting on" },
149
+ { from: "aws_s3_bucket_server_side_encryption_configuration", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "setting on" },
150
+ { from: "aws_s3_bucket_lifecycle_configuration", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "setting on" },
151
+ { from: "aws_s3_bucket_ownership_controls", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "setting on" },
152
+ { from: "aws_s3_bucket_acl", path: "bucket", targetKind: "aws_s3_bucket", relation: "reference", label: "grant on" },
153
+ { from: "aws_sqs_queue_policy", path: "queue_url", targetKind: "aws_sqs_queue", relation: "reference", label: "grant on" },
154
+ { from: "aws_lambda_event_source_mapping", path: "event_source_arn", relation: "reference", label: "drains" },
155
+ { from: "aws_lambda_event_source_mapping", path: "function_name", targetKind: "aws_lambda_function", relation: "reference", label: "invokes" },
156
+ { from: "aws_lambda_permission", path: "function_name", targetKind: "aws_lambda_function", relation: "reference", label: "grant on" },
157
+ { from: "aws_sns_topic_subscription", path: "topic_arn", targetKind: "aws_sns_topic", relation: "reference", label: "on" },
158
+ { from: "aws_cloudwatch_log_group", path: "kms_key_id", targetKind: "aws_kms_key", relation: "reference", label: "encrypted with" },
159
+ { from: "aws_sns_topic", path: "kms_master_key_id", targetKind: "aws_kms_key", relation: "reference", label: "encrypted with" },
160
+ { from: "aws_dynamodb_table", path: "server_side_encryption[].kms_key_arn", targetKind: "aws_kms_key", relation: "reference", label: "encrypted with" },
161
+ { from: "aws_iam_role_policy_attachment", path: "role", targetKind: "aws_iam_role", relation: "reference", label: "on" },
162
+ { from: "aws_iam_role_policy_attachment", path: "policy_arn", targetKind: "aws_iam_policy", relation: "reference", label: "attaches" },
163
+ { from: "aws_iam_role_policy", path: "role", targetKind: "aws_iam_role", relation: "reference", label: "on" },
164
+ { from: "aws_iam_instance_profile", path: "role", targetKind: "aws_iam_role", relation: "reference", label: "holds" },
165
+ { from: "aws_route53_record", path: "zone_id", targetKind: "aws_route53_zone", relation: "reference", label: "in zone" },
166
+ // A bucket's regional domain name or a load balancer's DNS name; no `targetKind`.
167
+ { from: "aws_cloudfront_distribution", path: "origin[].domain_name", relation: "reference", label: "origin" },
168
+ ],
169
+ };
170
+ /** Every provider type the catalog has at least one rule from. */
171
+ export const CATALOGUED_KINDS = new Set(TERRAFORM_REFERENCE_CATALOG.refs.map((r) => r.from));
172
+ /**
173
+ * True when nothing would be looked for on a node of this kind: no rule is
174
+ * written for it, and it is not one of the kinds known to reference nothing.
175
+ */
176
+ export function isUnresolvedKind(kind) {
177
+ return !CATALOGUED_KINDS.has(kind) && !REFERENCES_NOTHING.has(kind);
178
+ }