@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,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A behaviour engine's request from a terraform estate, declared or live (#2360).
|
|
3
|
+
*
|
|
4
|
+
* The epic (#2355) wants two predictions shown as a delta: what the file
|
|
5
|
+
* declares, and what the account holds. For a choudoufu root the second is
|
|
6
|
+
* readable — `live-ls -json` lists every resource the account holds under the
|
|
7
|
+
* estate's marker, `live-plan -json` says which declared instance each one is
|
|
8
|
+
* bound to and which declarations nothing answers for — and this module turns
|
|
9
|
+
* either reading into the request `PredictBehaviourOptions` describes:
|
|
10
|
+
* `entityNames`, `entities`, `edges`, `edgeCoverage`, `traffic`.
|
|
11
|
+
*
|
|
12
|
+
* One producer for both sides, on purpose. A delta is only a statement about
|
|
13
|
+
* the estate if both sides were assembled the same way, and the third review
|
|
14
|
+
* comment on #2360 is about exactly the asymmetry this avoids: the declared
|
|
15
|
+
* path had no containment producer, so a "one zone lost" verdict differed
|
|
16
|
+
* between the sides for a reason that was not drift. Here the same reference
|
|
17
|
+
* catalog (`./reference-catalog.ts`) runs over the same reconstruction
|
|
18
|
+
* (`reconstructEdges`, `packages/core/src/graph-refs.ts`) on both sides, and
|
|
19
|
+
* `edgeCoverage.containmentEdges` is populated the same way on each.
|
|
20
|
+
*
|
|
21
|
+
* ## What the live side is, and is not
|
|
22
|
+
*
|
|
23
|
+
* The two documents carry identities, types, addresses and markers. Neither
|
|
24
|
+
* carries a resource's attributes: a subnet's `vpc_id` as the account holds it
|
|
25
|
+
* is in no section of either. So a live node's arguments are the declaration's
|
|
26
|
+
* arguments — the body choudoufu applied — and its **references** are the
|
|
27
|
+
* declaration's references resolved against what is bound: a reference whose
|
|
28
|
+
* target the plan bound resolves to that target's live identity, and one whose
|
|
29
|
+
* target is absent from the account stays unresolved and is reported as
|
|
30
|
+
* `dangling`, naming the node, the argument and the address it points at. A
|
|
31
|
+
* resource the listing holds that nothing declares has no body at all; it is a
|
|
32
|
+
* node with an identity and a type, and its kind is counted into
|
|
33
|
+
* `unresolvedKinds` unless the catalog knows that kind references nothing.
|
|
34
|
+
*
|
|
35
|
+
* The drift this side sees is therefore **membership** drift: a resource added
|
|
36
|
+
* out of band under the estate's marker, a declared resource never applied, a
|
|
37
|
+
* resource at a declared identity that another estate holds. An attribute
|
|
38
|
+
* changed live — an instance resized by hand — is not in either document and
|
|
39
|
+
* is not claimed here; the size an engine is sent is the declared one on both
|
|
40
|
+
* sides. `describe-resources.ts`'s module doc has the same boundary for the
|
|
41
|
+
* observation read, for the same reason.
|
|
42
|
+
*
|
|
43
|
+
* One consequence of that worth stating outright, because it is the one a
|
|
44
|
+
* reader will hit first: a block with `count` or `for_each` is **one** node on
|
|
45
|
+
* both sides, whatever the account holds. The request's unit is the entity the
|
|
46
|
+
* caller asked about, and the caller asked about `aws_eip.pool`, not about two
|
|
47
|
+
* slots — inventing `aws_eip.pool[0]` and `[1]` as entities would put names in
|
|
48
|
+
* the report that are in no `entityNames` list and in no build. Every instance
|
|
49
|
+
* address the plan named rides on `props.live.instances` instead, so an engine
|
|
50
|
+
* that prices per instance has the count and one that does not is not silently
|
|
51
|
+
* handed a multiplier. An estate whose drift is a changed `count` is therefore
|
|
52
|
+
* a delta this path does not yet show.
|
|
53
|
+
*
|
|
54
|
+
* ## `edgeCoverage`, honestly
|
|
55
|
+
*
|
|
56
|
+
* `partial` whenever `dangling` or `unresolvedKinds` is non-empty, `complete`
|
|
57
|
+
* only when both are empty, and `dangling` carries `reconstructEdges`'s
|
|
58
|
+
* `DanglingRef` records through unflattened — `from` is the field that says
|
|
59
|
+
* which node's argument leaves the estate. `containmentEdges` comes from the
|
|
60
|
+
* reconstruction's `containmentEdges`, the traversable shape, and never from
|
|
61
|
+
* the `containment` pairs beside it. All three of those are #2360's review
|
|
62
|
+
* comments, applied.
|
|
63
|
+
*/
|
|
64
|
+
import { reconstructEdges } from "@intentius/chant/graph-refs";
|
|
65
|
+
import { classifyLiveInstance, declaredOf, entityKeyFor, instancesOf, } from "../describe-resources.js";
|
|
66
|
+
import { RESOURCE_TYPE } from "../hcl/parse.js";
|
|
67
|
+
import { isUnresolvedKind, TERRAFORM_REFERENCE_CATALOG } from "./reference-catalog.js";
|
|
68
|
+
/** Code-unit order, so the request's bytes do not depend on the machine's locale. */
|
|
69
|
+
const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
70
|
+
/** The block a document instance address belongs to: `aws_eip.pool[0]` is `aws_eip.pool`. */
|
|
71
|
+
export function blockAddressOf(instance) {
|
|
72
|
+
return instance.replace(/\[[^\]]*\]$/, "");
|
|
73
|
+
}
|
|
74
|
+
/** The region an ARN names, or undefined for a global one (`arn:aws:s3:::bucket`). */
|
|
75
|
+
export function regionOfArn(arn) {
|
|
76
|
+
const parts = arn.split(":");
|
|
77
|
+
return parts[0] === "arn" && parts.length >= 6 && parts[3] ? parts[3] : undefined;
|
|
78
|
+
}
|
|
79
|
+
function asString(value) {
|
|
80
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
function bodyOf(props) {
|
|
83
|
+
const body = props.body;
|
|
84
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : {};
|
|
85
|
+
}
|
|
86
|
+
function referencesOf(entity) {
|
|
87
|
+
return Array.isArray(entity?.references) ? entity.references : [];
|
|
88
|
+
}
|
|
89
|
+
/** True when a body holds any `${…}` string at all, at any depth. */
|
|
90
|
+
function holdsReference(value, depth = 0) {
|
|
91
|
+
if (depth > 12)
|
|
92
|
+
return false;
|
|
93
|
+
if (typeof value === "string")
|
|
94
|
+
return value.includes("${");
|
|
95
|
+
if (Array.isArray(value))
|
|
96
|
+
return value.some((v) => holdsReference(v, depth + 1));
|
|
97
|
+
if (typeof value === "object" && value !== null) {
|
|
98
|
+
return Object.values(value).some((v) => holdsReference(v, depth + 1));
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/** `${addr}` followed by an attribute, an index, or the closing brace — the address itself, not a longer one. */
|
|
103
|
+
function mentions(text, address) {
|
|
104
|
+
const escaped = address.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
return new RegExp(`\\$\\{${escaped}(?:[.\\[}\\s)\\],]|$)`).test(text);
|
|
106
|
+
}
|
|
107
|
+
/** One `${…}` and nothing else: the value the account holds *is* the target's identity. */
|
|
108
|
+
const WHOLE_INTERPOLATION = /^\$\{[^{}]*\}$/;
|
|
109
|
+
/**
|
|
110
|
+
* The body with every resolvable reference replaced by the identifier it
|
|
111
|
+
* stands for, so `reconstructEdges` can match it against the node that owns
|
|
112
|
+
* that identifier.
|
|
113
|
+
*
|
|
114
|
+
* A leaf string is replaced only when the reference **is** the whole value:
|
|
115
|
+
* the string is one `${…}` and nothing else, exactly one of the block's
|
|
116
|
+
* references under the same top-level argument is the one it names, and that
|
|
117
|
+
* reference resolves. Then the value in the account really is the target's
|
|
118
|
+
* identity, and matching it against the node holding that identity is a
|
|
119
|
+
* statement about the estate.
|
|
120
|
+
*
|
|
121
|
+
* Everything else is left alone, and the two cases it leaves alone are
|
|
122
|
+
* different. `"${a.id}-${b.id}"` and `"${a.id}-suffix"` are composed values —
|
|
123
|
+
* a name derived from an identity is not that identity, and substituting the
|
|
124
|
+
* bare identity into one would hand the resolver a value the account does not
|
|
125
|
+
* hold and get back an edge nothing points along. A string naming a reference
|
|
126
|
+
* that does not resolve keeps its `${…}`, which is what makes a reference to a
|
|
127
|
+
* target the account is missing come back from the resolver as `dangling` with
|
|
128
|
+
* the address in `value` rather than vanishing.
|
|
129
|
+
*/
|
|
130
|
+
export function substituteReferences(body, references, addressOf, resolve) {
|
|
131
|
+
const leaf = (value, via) => {
|
|
132
|
+
if (!WHOLE_INTERPOLATION.test(value))
|
|
133
|
+
return value;
|
|
134
|
+
const matched = references.filter((r) => {
|
|
135
|
+
if ((r.viaAttr ?? "") !== via)
|
|
136
|
+
return false;
|
|
137
|
+
const address = addressOf(r.to);
|
|
138
|
+
return address !== undefined && mentions(value, address);
|
|
139
|
+
});
|
|
140
|
+
if (matched.length !== 1)
|
|
141
|
+
return value;
|
|
142
|
+
return resolve(matched[0].to) ?? value;
|
|
143
|
+
};
|
|
144
|
+
const walk = (value, via, depth) => {
|
|
145
|
+
if (depth > 12)
|
|
146
|
+
return value;
|
|
147
|
+
if (typeof value === "string")
|
|
148
|
+
return leaf(value, via);
|
|
149
|
+
if (Array.isArray(value))
|
|
150
|
+
return value.map((v) => walk(v, via, depth + 1));
|
|
151
|
+
if (typeof value === "object" && value !== null) {
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
154
|
+
out[key] = walk(inner, depth === 0 ? key : via, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
};
|
|
160
|
+
return walk(body, "", 0);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The provider type a resource block declares — `aws_subnet` — or undefined
|
|
164
|
+
* for any other block.
|
|
165
|
+
*
|
|
166
|
+
* Read off the address, because the block carries no `type` field:
|
|
167
|
+
* `../hcl/parse.ts` composes a resource's address as `${type}.${name}` and
|
|
168
|
+
* keeps it unqualified even inside a descended child module, where the calling
|
|
169
|
+
* chain lives on `props.callers` and the `module.<name>` segments live in the
|
|
170
|
+
* entity's key. An address with no dot is a live row's identity standing in
|
|
171
|
+
* for an address it had none of, and is not a type.
|
|
172
|
+
*
|
|
173
|
+
* This is the kind a node is given for `reconstructEdges`, and it is the same
|
|
174
|
+
* derivation `./kinds.ts`'s `resolveType` runs to look a coverage row up,
|
|
175
|
+
* so the graph and the coverage table agree on what a block is. A live row
|
|
176
|
+
* states its own type instead, on `props.resourceType`, the name
|
|
177
|
+
* `observeAmbient` already uses for it.
|
|
178
|
+
*/
|
|
179
|
+
function providerTypeOf(entity) {
|
|
180
|
+
if (entity.entityType !== RESOURCE_TYPE)
|
|
181
|
+
return undefined;
|
|
182
|
+
const stated = asString(entity.props.resourceType);
|
|
183
|
+
if (stated !== undefined)
|
|
184
|
+
return stated;
|
|
185
|
+
const address = asString(entity.props.address);
|
|
186
|
+
if (address === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
const dot = address.indexOf(".");
|
|
189
|
+
return dot > 0 ? address.slice(0, dot) : undefined;
|
|
190
|
+
}
|
|
191
|
+
function emptyPart() {
|
|
192
|
+
return { nodes: [], entities: new Map(), sources: {}, absent: [], unpredicted: {}, unresolvedKinds: new Set() };
|
|
193
|
+
}
|
|
194
|
+
function liveVerdictOf(address, plan) {
|
|
195
|
+
const instances = instancesOf(address, plan);
|
|
196
|
+
if (instances.length === 0)
|
|
197
|
+
return { kind: "absent", instances };
|
|
198
|
+
const verdicts = instances.map((a) => classifyLiveInstance(a, plan));
|
|
199
|
+
const unobserved = verdicts.find((v) => v.kind === "unobserved");
|
|
200
|
+
if (unobserved && unobserved.kind === "unobserved") {
|
|
201
|
+
return {
|
|
202
|
+
kind: "unobserved",
|
|
203
|
+
instances,
|
|
204
|
+
// `no-credentials` is an observation reason and not a prediction one;
|
|
205
|
+
// the credential wording stays in the detail.
|
|
206
|
+
reason: unobserved.reason === "no-credentials" ? "read-failed" : unobserved.reason,
|
|
207
|
+
detail: unobserved.detail,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const foreign = verdicts.find((v) => v.kind === "foreign");
|
|
211
|
+
if (foreign && foreign.kind === "foreign") {
|
|
212
|
+
return {
|
|
213
|
+
kind: "present",
|
|
214
|
+
status: "foreign",
|
|
215
|
+
ownership: "foreign",
|
|
216
|
+
instances,
|
|
217
|
+
...(foreign.row.identity ? { identity: foreign.row.identity } : {}),
|
|
218
|
+
...(foreign.row.heldBy ? { heldBy: foreign.row.heldBy } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const adoptable = verdicts.find((v) => v.kind === "adoptable");
|
|
222
|
+
if (adoptable && adoptable.kind === "adoptable") {
|
|
223
|
+
return {
|
|
224
|
+
kind: "present",
|
|
225
|
+
status: "adoptable",
|
|
226
|
+
ownership: "unknown",
|
|
227
|
+
instances,
|
|
228
|
+
...(adoptable.row.identity ? { identity: adoptable.row.identity } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const owned = verdicts.filter((v) => v.kind === "owned");
|
|
232
|
+
if (owned.length === 0)
|
|
233
|
+
return { kind: "absent", instances };
|
|
234
|
+
const single = owned.length === 1 && instances.length === 1 ? owned[0] : undefined;
|
|
235
|
+
return {
|
|
236
|
+
kind: "present",
|
|
237
|
+
status: "bound",
|
|
238
|
+
ownership: "owned",
|
|
239
|
+
instances,
|
|
240
|
+
...(single && single.kind === "owned" && single.row.identity ? { identity: single.row.identity } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A live root: the declared resources the account holds, at their live
|
|
245
|
+
* identities, plus everything the listing holds that nothing declares.
|
|
246
|
+
*/
|
|
247
|
+
function liveRootPart(root, declared, entities, read, owned) {
|
|
248
|
+
const part = emptyPart();
|
|
249
|
+
const { listing, plan } = read;
|
|
250
|
+
const estate = plan.estate || listing.estate;
|
|
251
|
+
// The listing, by block address: a `count` block has one item per instance.
|
|
252
|
+
const listed = new Map();
|
|
253
|
+
for (const item of listing.items) {
|
|
254
|
+
if (!item.address)
|
|
255
|
+
continue;
|
|
256
|
+
const block = blockAddressOf(item.address);
|
|
257
|
+
(listed.get(block) ?? listed.set(block, []).get(block)).push(item);
|
|
258
|
+
}
|
|
259
|
+
// Live identity per entity key, for reference substitution below. Filled as
|
|
260
|
+
// declared blocks are placed; an orphan can be the target of nothing.
|
|
261
|
+
const identity = new Map();
|
|
262
|
+
const placed = [];
|
|
263
|
+
for (const d of declared) {
|
|
264
|
+
const entity = entities.get(d.name);
|
|
265
|
+
if (!entity)
|
|
266
|
+
continue;
|
|
267
|
+
if (entity.entityType !== RESOURCE_TYPE) {
|
|
268
|
+
// A variable, an output, a data block: not a thing the account holds,
|
|
269
|
+
// so the account has nothing to say about it. It rides through as
|
|
270
|
+
// declared, and the coverage rows decline it by name on both sides alike.
|
|
271
|
+
part.sources[d.name] = "live";
|
|
272
|
+
part.entities.set(d.name, entity);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const verdict = liveVerdictOf(d.address, plan);
|
|
276
|
+
if (verdict.kind === "absent") {
|
|
277
|
+
part.absent.push(d.name);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (verdict.kind === "unobserved") {
|
|
281
|
+
part.unpredicted[d.name] = { type: RESOURCE_TYPE, reason: verdict.reason, detail: verdict.detail };
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (owned && verdict.ownership !== "owned") {
|
|
285
|
+
part.unpredicted[d.name] = {
|
|
286
|
+
type: RESOURCE_TYPE,
|
|
287
|
+
reason: "filtered",
|
|
288
|
+
detail: `this address read \`${verdict.ownership}\` on the root's live markers and --owned was requested`,
|
|
289
|
+
};
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const items = listed.get(d.address) ?? [];
|
|
293
|
+
const single = items.length === 1 && verdict.instances.length === 1 ? items[0] : undefined;
|
|
294
|
+
const facts = {
|
|
295
|
+
status: verdict.status,
|
|
296
|
+
ownership: verdict.ownership,
|
|
297
|
+
...(verdict.identity ? { identity: verdict.identity } : {}),
|
|
298
|
+
...(single ? { arn: single.id } : {}),
|
|
299
|
+
...(single && regionOfArn(single.id) ? { region: regionOfArn(single.id) } : {}),
|
|
300
|
+
...(verdict.instances.length > 1 || verdict.instances[0] !== d.address ? { instances: verdict.instances } : {}),
|
|
301
|
+
...(verdict.heldBy ? { heldBy: verdict.heldBy } : {}),
|
|
302
|
+
};
|
|
303
|
+
identity.set(d.name, verdict.identity);
|
|
304
|
+
part.sources[d.name] = "live";
|
|
305
|
+
placed.push({ name: d.name, entity, declared: d, facts });
|
|
306
|
+
}
|
|
307
|
+
const addressOf = (to) => asString(entities.get(to)?.props.address);
|
|
308
|
+
// A bound target resolves to its live identity; a target that is live but
|
|
309
|
+
// carries none (a marker read the tagging index had not settled, choudoufu
|
|
310
|
+
// #1014) resolves to its node id; a target not in the account resolves to
|
|
311
|
+
// nothing, and the reference stays in the body for the resolver to report.
|
|
312
|
+
const resolve = (to) => identity.has(to) ? (identity.get(to) ?? to) : undefined;
|
|
313
|
+
for (const { name, entity, declared: d, facts } of placed) {
|
|
314
|
+
const kind = providerTypeOf(entity) ?? "";
|
|
315
|
+
const references = referencesOf(entity);
|
|
316
|
+
const body = bodyOf(entity.props);
|
|
317
|
+
if (references.length === 0 && holdsReference(body))
|
|
318
|
+
part.unresolvedKinds.add(kind);
|
|
319
|
+
else if (isUnresolvedKind(kind))
|
|
320
|
+
part.unresolvedKinds.add(kind);
|
|
321
|
+
part.nodes.push({
|
|
322
|
+
id: name,
|
|
323
|
+
kind,
|
|
324
|
+
lexicon: "terraform",
|
|
325
|
+
attrs: substituteReferences(body, references, addressOf, resolve),
|
|
326
|
+
...(facts.identity ? { physicalId: facts.identity } : {}),
|
|
327
|
+
});
|
|
328
|
+
part.entities.set(name, {
|
|
329
|
+
entityType: entity.entityType,
|
|
330
|
+
props: { ...entity.props, address: d.address, live: facts },
|
|
331
|
+
...(entity.references ? { references: entity.references } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// What the account holds that nothing declares: the owned orphans. No body,
|
|
335
|
+
// so nothing to look for references in.
|
|
336
|
+
for (const item of listing.items) {
|
|
337
|
+
if (item.declared)
|
|
338
|
+
continue;
|
|
339
|
+
const address = item.address ? blockAddressOf(item.address) : item.id;
|
|
340
|
+
const name = entityKeyFor(root, address);
|
|
341
|
+
if (part.entities.has(name) || entities.has(name))
|
|
342
|
+
continue;
|
|
343
|
+
const facts = {
|
|
344
|
+
status: "orphan",
|
|
345
|
+
ownership: "owned",
|
|
346
|
+
arn: item.id,
|
|
347
|
+
...(regionOfArn(item.id) ? { region: regionOfArn(item.id) } : {}),
|
|
348
|
+
...(item.address && item.address !== address ? { instances: [item.address] } : {}),
|
|
349
|
+
tags: item.tags,
|
|
350
|
+
};
|
|
351
|
+
if (isUnresolvedKind(item.type))
|
|
352
|
+
part.unresolvedKinds.add(item.type);
|
|
353
|
+
part.sources[name] = "live";
|
|
354
|
+
part.nodes.push({ id: name, kind: item.type, lexicon: "terraform", attrs: {}, physicalId: item.id });
|
|
355
|
+
part.entities.set(name, {
|
|
356
|
+
entityType: RESOURCE_TYPE,
|
|
357
|
+
props: {
|
|
358
|
+
address,
|
|
359
|
+
root,
|
|
360
|
+
estate,
|
|
361
|
+
// `resourceType`, the name `observeAmbient` gives the same fact
|
|
362
|
+
// (`../describe-resources.ts`). An undeclared resource has no
|
|
363
|
+
// terraform address for a type to be read out of, so the listing's
|
|
364
|
+
// own statement of it is the only one there is.
|
|
365
|
+
resourceType: item.type,
|
|
366
|
+
mode: "live",
|
|
367
|
+
live: facts,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return part;
|
|
372
|
+
}
|
|
373
|
+
/** A declared root: every entity as declared, references resolved to the entity keys they name. */
|
|
374
|
+
function declaredRootPart(declared, entities) {
|
|
375
|
+
const part = emptyPart();
|
|
376
|
+
const addressOf = (to) => asString(entities.get(to)?.props.address);
|
|
377
|
+
const resolve = (to) => (entities.has(to) ? to : undefined);
|
|
378
|
+
for (const d of declared) {
|
|
379
|
+
const entity = entities.get(d.name);
|
|
380
|
+
if (!entity)
|
|
381
|
+
continue;
|
|
382
|
+
part.sources[d.name] = "declared";
|
|
383
|
+
part.entities.set(d.name, entity);
|
|
384
|
+
const kind = providerTypeOf(entity);
|
|
385
|
+
if (kind === undefined)
|
|
386
|
+
continue;
|
|
387
|
+
const references = referencesOf(entity);
|
|
388
|
+
const body = bodyOf(entity.props);
|
|
389
|
+
if (references.length === 0 && holdsReference(body))
|
|
390
|
+
part.unresolvedKinds.add(kind);
|
|
391
|
+
else if (isUnresolvedKind(kind))
|
|
392
|
+
part.unresolvedKinds.add(kind);
|
|
393
|
+
part.nodes.push({
|
|
394
|
+
id: d.name,
|
|
395
|
+
kind,
|
|
396
|
+
lexicon: "terraform",
|
|
397
|
+
attrs: substituteReferences(body, references, addressOf, resolve),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return part;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The coverage claim, from the reconstruction and the kinds nothing was
|
|
404
|
+
* looked for on. `partial` names what is missing; `complete` is claimed only
|
|
405
|
+
* when nothing is.
|
|
406
|
+
*/
|
|
407
|
+
export function edgeCoverageOf(reconstructed, unresolvedKinds) {
|
|
408
|
+
const dangling = reconstructed.dangling;
|
|
409
|
+
const kinds = [...unresolvedKinds].sort(byCodeUnit);
|
|
410
|
+
return {
|
|
411
|
+
verdict: dangling.length > 0 || kinds.length > 0 ? "partial" : "complete",
|
|
412
|
+
...(dangling.length > 0 ? { dangling } : {}),
|
|
413
|
+
...(kinds.length > 0 ? { unresolvedKinds: kinds } : {}),
|
|
414
|
+
containmentEdges: reconstructed.containmentEdges,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Build the request. Pure: the documents are already parsed and nothing here
|
|
419
|
+
* reads a clock, the environment or the filesystem, so the same declaration
|
|
420
|
+
* and the same two documents give the same request forever.
|
|
421
|
+
*/
|
|
422
|
+
export function terraformBehaviourRequest(options) {
|
|
423
|
+
const live = new Map((options.live ?? []).map((read) => [read.root, read]));
|
|
424
|
+
const byRoot = new Map();
|
|
425
|
+
const rootless = [];
|
|
426
|
+
for (const name of options.entityNames) {
|
|
427
|
+
const entity = options.entities.get(name);
|
|
428
|
+
const d = declaredOf(name, entity);
|
|
429
|
+
if (!d.root) {
|
|
430
|
+
rootless.push(name);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
(byRoot.get(d.root) ?? byRoot.set(d.root, []).get(d.root)).push(d);
|
|
434
|
+
}
|
|
435
|
+
const parts = [];
|
|
436
|
+
for (const [root, declared] of byRoot) {
|
|
437
|
+
const read = live.get(root);
|
|
438
|
+
parts.push(read ? liveRootPart(root, declared, options.entities, read, options.owned) : declaredRootPart(declared, options.entities));
|
|
439
|
+
}
|
|
440
|
+
const entities = new Map();
|
|
441
|
+
const sources = {};
|
|
442
|
+
const absent = [];
|
|
443
|
+
const unpredicted = {};
|
|
444
|
+
const unresolved = new Set();
|
|
445
|
+
const nodes = [];
|
|
446
|
+
for (const part of parts) {
|
|
447
|
+
for (const [name, entity] of part.entities)
|
|
448
|
+
entities.set(name, entity);
|
|
449
|
+
Object.assign(sources, part.sources);
|
|
450
|
+
absent.push(...part.absent);
|
|
451
|
+
Object.assign(unpredicted, part.unpredicted);
|
|
452
|
+
for (const kind of part.unresolvedKinds)
|
|
453
|
+
unresolved.add(kind);
|
|
454
|
+
nodes.push(...part.nodes);
|
|
455
|
+
}
|
|
456
|
+
// A name carrying no root came from somewhere other than `buildRoots()`. It
|
|
457
|
+
// is passed through as it came, so the engine-fronting lexicon reports it
|
|
458
|
+
// rather than this builder dropping it.
|
|
459
|
+
for (const name of rootless) {
|
|
460
|
+
const entity = options.entities.get(name);
|
|
461
|
+
if (entity)
|
|
462
|
+
entities.set(name, entity);
|
|
463
|
+
sources[name] = "declared";
|
|
464
|
+
}
|
|
465
|
+
nodes.sort((a, b) => byCodeUnit(a.id, b.id));
|
|
466
|
+
const reconstructed = reconstructEdges(nodes, TERRAFORM_REFERENCE_CATALOG);
|
|
467
|
+
const entityNames = [...new Set([...entities.keys(), ...rootless])].sort(byCodeUnit);
|
|
468
|
+
const request = {
|
|
469
|
+
environment: options.environment,
|
|
470
|
+
buildOutput: options.buildOutput,
|
|
471
|
+
entityNames,
|
|
472
|
+
entities: entities,
|
|
473
|
+
...(options.stack !== undefined ? { stack: options.stack } : {}),
|
|
474
|
+
...(options.region !== undefined ? { region: options.region } : {}),
|
|
475
|
+
...(options.owned !== undefined ? { owned: options.owned } : {}),
|
|
476
|
+
from: options.from,
|
|
477
|
+
traffic: options.traffic,
|
|
478
|
+
edges: reconstructed.edges,
|
|
479
|
+
edgeCoverage: edgeCoverageOf(reconstructed, [...unresolved]),
|
|
480
|
+
};
|
|
481
|
+
return { request, sources, absent: absent.sort(byCodeUnit), unpredicted };
|
|
482
|
+
}
|