@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,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
|
+
}
|