@intentius/chant 0.44.6 → 0.44.7
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/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts +3 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/carve.d.ts +7 -2
- package/dist/cli/commands/carve.d.ts.map +1 -1
- package/dist/terraform/adopt-state.d.ts +18 -1
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/aws-resources.d.ts +29 -0
- package/dist/terraform/aws-resources.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts +8 -0
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/carve.d.ts +8 -3
- package/dist/terraform/carve.d.ts.map +1 -1
- package/dist/terraform/graph.d.ts +10 -4
- package/dist/terraform/graph.d.ts.map +1 -1
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/terraform/score.d.ts +11 -0
- package/dist/terraform/score.d.ts.map +1 -1
- package/dist/terraform/types.d.ts +11 -0
- package/dist/terraform/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +32 -0
- package/src/cli/commands/carve-bridge.ts +3 -0
- package/src/cli/commands/carve-emit-state.test.ts +107 -0
- package/src/cli/commands/carve-emit.ts +37 -5
- package/src/cli/commands/carve.test.ts +39 -1
- package/src/cli/commands/carve.ts +8 -2
- package/src/terraform/__fixtures__/advise.test.ts +6 -4
- package/src/terraform/adopt-state.test.ts +64 -0
- package/src/terraform/adopt-state.ts +52 -2
- package/src/terraform/aws-resources.test.ts +102 -1
- package/src/terraform/aws-resources.ts +144 -2
- package/src/terraform/bridge.test.ts +30 -0
- package/src/terraform/bridge.ts +19 -1
- package/src/terraform/carve.test.ts +34 -0
- package/src/terraform/carve.ts +0 -0
- package/src/terraform/graph.test.ts +45 -0
- package/src/terraform/graph.ts +35 -16
- package/src/terraform/parse.ts +4 -1
- package/src/terraform/score.test.ts +25 -0
- package/src/terraform/score.ts +27 -4
- package/src/terraform/types.ts +11 -0
|
@@ -87,6 +87,40 @@ describe("boundaryReport", () => {
|
|
|
87
87
|
]);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("an output block is an inbound edge with its own bridge kind (#1638)", () => {
|
|
91
|
+
const tree: Hcl2JsonTree = {
|
|
92
|
+
resource: { aws_s3_bucket: { assets: [{ bucket: "b" }] } },
|
|
93
|
+
output: { assets_bucket: [{ value: "${aws_s3_bucket.assets.bucket}" }] },
|
|
94
|
+
};
|
|
95
|
+
const report = boundaryReport(buildFixtureGraph(tree), "aws_s3_bucket.assets")!;
|
|
96
|
+
expect(report.inbound).toEqual([
|
|
97
|
+
{
|
|
98
|
+
direction: "inbound",
|
|
99
|
+
survivor: "output.assets_bucket",
|
|
100
|
+
carved: "aws_s3_bucket.assets",
|
|
101
|
+
attrs: ["bucket"],
|
|
102
|
+
via: ["value"],
|
|
103
|
+
bridge: "tf-output-rewrite",
|
|
104
|
+
required: "immediately",
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
expect(report.peelability).toBe(96); // the cheaper output weight, not 88
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("an output on a folded sub-resource is still the parent's boundary work", () => {
|
|
111
|
+
const tree: Hcl2JsonTree = {
|
|
112
|
+
resource: {
|
|
113
|
+
aws_s3_bucket: { assets: [{ bucket: "b" }] },
|
|
114
|
+
aws_s3_bucket_versioning: { assets: [{ bucket: "${aws_s3_bucket.assets.id}" }] },
|
|
115
|
+
},
|
|
116
|
+
output: { versioning_id: [{ value: "${aws_s3_bucket_versioning.assets.id}" }] },
|
|
117
|
+
};
|
|
118
|
+
const report = boundaryReport(buildFixtureGraph(tree), "aws_s3_bucket.assets")!;
|
|
119
|
+
expect(report.inbound.map((e) => [e.survivor, e.carved, e.bridge])).toEqual([
|
|
120
|
+
["output.versioning_id", "aws_s3_bucket_versioning.assets", "tf-output-rewrite"],
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
90
124
|
test("unsupported type is flagged in diagnostics", () => {
|
|
91
125
|
const tree: Hcl2JsonTree = { resource: { random_pet: { n: [{ length: 2 }] } } };
|
|
92
126
|
const report = boundaryReport(buildFixtureGraph(tree), "random_pet.n")!;
|
package/src/terraform/carve.ts
CHANGED
|
Binary file
|
|
@@ -157,6 +157,51 @@ describe("buildGraph", () => {
|
|
|
157
157
|
expect(byAddr["aws_s3_bucket.assets"].identity).toBe("myapp-assets-prod"); // flat attr unaffected
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
test("an output block referencing a resource is an inbound edge, tagged as an output (#1638)", () => {
|
|
161
|
+
const tree: Hcl2JsonTree = {
|
|
162
|
+
resource: { aws_s3_bucket: { assets: [{ bucket: "x" }] } },
|
|
163
|
+
output: {
|
|
164
|
+
assets_bucket: [{ value: "${aws_s3_bucket.assets.bucket}" }],
|
|
165
|
+
assets_pair: [{ value: ["${aws_s3_bucket.assets.arn}", "${aws_s3_bucket.assets.id}"], description: "both" }],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const g = buildFixtureGraph(tree);
|
|
169
|
+
|
|
170
|
+
// An output is a referrer, never a node — nothing carves an output.
|
|
171
|
+
expect(g.nodes.map((n) => n.address)).toEqual(["aws_s3_bucket.assets"]);
|
|
172
|
+
expect(inboundEdges(g, "aws_s3_bucket.assets")).toEqual([
|
|
173
|
+
{
|
|
174
|
+
from: "output.assets_bucket",
|
|
175
|
+
to: "aws_s3_bucket.assets",
|
|
176
|
+
attrs: ["bucket"],
|
|
177
|
+
via: ["value"],
|
|
178
|
+
fromKind: "output",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
from: "output.assets_pair",
|
|
182
|
+
to: "aws_s3_bucket.assets",
|
|
183
|
+
attrs: ["arn", "id"],
|
|
184
|
+
via: ["value"],
|
|
185
|
+
fromKind: "output",
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
// Nothing can depend on an output in turn.
|
|
189
|
+
expect(outboundEdges(g, "output.assets_bucket").map((e) => e.to)).toEqual(["aws_s3_bucket.assets"]);
|
|
190
|
+
expect(inboundEdges(g, "output.assets_bucket")).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("an output referencing a var or a data source contributes no edge", () => {
|
|
194
|
+
const tree: Hcl2JsonTree = {
|
|
195
|
+
data: { aws_ami: { ubuntu: [{ owners: ["1"] }] } },
|
|
196
|
+
resource: { aws_s3_bucket: { assets: [{ bucket: "x" }] } },
|
|
197
|
+
output: {
|
|
198
|
+
name: [{ value: "${var.name}" }],
|
|
199
|
+
ami: [{ value: "${data.aws_ami.ubuntu.id}" }],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
expect(buildFixtureGraph(tree).edges).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
160
205
|
test("var / local references are not edges", () => {
|
|
161
206
|
const tree: Hcl2JsonTree = {
|
|
162
207
|
resource: {
|
package/src/terraform/graph.ts
CHANGED
|
@@ -81,9 +81,9 @@ export function refFromAccessor(accessor: string): RawRef | null {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* Every string value carrying an interpolation across the tree's resource
|
|
85
|
-
* module blocks — exactly the expressions `parse.ts` must resolve
|
|
86
|
-
* AST before `buildGraph` can classify them.
|
|
84
|
+
* Every string value carrying an interpolation across the tree's resource,
|
|
85
|
+
* module and output blocks — exactly the expressions `parse.ts` must resolve
|
|
86
|
+
* through the AST before `buildGraph` can classify them.
|
|
87
87
|
*/
|
|
88
88
|
export function collectExpressions(tree: Hcl2JsonTree): string[] {
|
|
89
89
|
const exprs = new Set<string>();
|
|
@@ -98,6 +98,7 @@ export function collectExpressions(tree: Hcl2JsonTree): string[] {
|
|
|
98
98
|
};
|
|
99
99
|
for (const named of Object.values(tree.resource ?? {})) for (const blocks of Object.values(named)) visit(blocks);
|
|
100
100
|
for (const blocks of Object.values(tree.module ?? {})) visit(blocks);
|
|
101
|
+
for (const blocks of Object.values(tree.output ?? {})) visit(blocks);
|
|
101
102
|
return [...exprs].sort();
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -167,6 +168,12 @@ function literalIdentity(block: unknown, type: string): string | undefined {
|
|
|
167
168
|
* marks the referring node dynamic. An edge is recorded only when its target
|
|
168
169
|
* resolves to a known resource/module node — references to `var`/`local`/data
|
|
169
170
|
* are dropped.
|
|
171
|
+
*
|
|
172
|
+
* `output` blocks are not nodes either — nothing carves an output — but they
|
|
173
|
+
* do reference, and a reference to a carved resource breaks the surviving plan
|
|
174
|
+
* exactly like a resource's does. So an output contributes an edge tagged
|
|
175
|
+
* `fromKind: "output"` from the pseudo-address `output.<name>` (#1638),
|
|
176
|
+
* which the scorer weights lower and `carve bridge` patches.
|
|
170
177
|
*/
|
|
171
178
|
export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGraph {
|
|
172
179
|
const nodes: TfNode[] = [];
|
|
@@ -216,22 +223,34 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
|
|
|
216
223
|
});
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
// Output blocks: referrers without being nodes (#1638). Their pseudo-address
|
|
227
|
+
// never joins `known`, so nothing can depend on an output in turn.
|
|
228
|
+
const outputRefs = new Map<string, RawRef[]>();
|
|
229
|
+
for (const [name, blocks] of Object.entries(tree.output ?? {})) {
|
|
230
|
+
const block = Array.isArray(blocks) ? blocks[0] : blocks;
|
|
231
|
+
outputRefs.set(`output.${name}`, refsInBlock(block, exprRefs));
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
// Edges: keep only references that resolve to a known node.
|
|
220
235
|
const known = new Set(nodes.map((n) => n.address));
|
|
221
236
|
const edges: TfEdge[] = [];
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
237
|
+
const collect = (source: Map<string, RawRef[]>, fromKind?: "output"): void => {
|
|
238
|
+
for (const [from, refs] of source) {
|
|
239
|
+
const byTarget = new Map<string, { attrs: Set<string>; via: Set<string> }>();
|
|
240
|
+
for (const ref of refs) {
|
|
241
|
+
if (ref.address === from || !known.has(ref.address)) continue;
|
|
242
|
+
if (!byTarget.has(ref.address)) byTarget.set(ref.address, { attrs: new Set(), via: new Set() });
|
|
243
|
+
const target = byTarget.get(ref.address)!;
|
|
244
|
+
if (ref.attr) target.attrs.add(ref.attr);
|
|
245
|
+
if (ref.via) target.via.add(ref.via);
|
|
246
|
+
}
|
|
247
|
+
for (const [to, { attrs, via }] of byTarget) {
|
|
248
|
+
edges.push({ from, to, attrs: [...attrs].sort(), via: [...via].sort(), ...(fromKind ? { fromKind } : {}) });
|
|
249
|
+
}
|
|
233
250
|
}
|
|
234
|
-
}
|
|
251
|
+
};
|
|
252
|
+
collect(rawRefsByNode);
|
|
253
|
+
collect(outputRefs, "output");
|
|
235
254
|
|
|
236
255
|
// Code-point ordering (not localeCompare) so output is locale-independent and
|
|
237
256
|
// punctuation sorts predictably (`.` < `_`).
|
|
@@ -242,7 +261,7 @@ export function buildGraph(tree: Hcl2JsonTree, exprRefs: ExpressionRefs): TfGrap
|
|
|
242
261
|
};
|
|
243
262
|
}
|
|
244
263
|
|
|
245
|
-
/** Edges where
|
|
264
|
+
/** Edges where something in the surviving Terraform depends on `address`. */
|
|
246
265
|
export function inboundEdges(graph: TfGraph, address: string): TfEdge[] {
|
|
247
266
|
return graph.edges.filter((e) => e.to === address);
|
|
248
267
|
}
|
package/src/terraform/parse.ts
CHANGED
|
@@ -68,7 +68,7 @@ async function resolveExpressionRefs(hcl2json: Hcl2Json, tree: Hcl2JsonTree): Pr
|
|
|
68
68
|
return refs;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Deep-merge hcl2json trees across files (resource/module/data namespaces). */
|
|
71
|
+
/** Deep-merge hcl2json trees across files (resource/module/data/output namespaces). */
|
|
72
72
|
function mergeTrees(into: Hcl2JsonTree, next: Hcl2JsonTree): void {
|
|
73
73
|
for (const section of ["resource", "data"] as const) {
|
|
74
74
|
const src = next[section];
|
|
@@ -79,6 +79,9 @@ function mergeTrees(into: Hcl2JsonTree, next: Hcl2JsonTree): void {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
if (next.module) into.module = { ...(into.module ?? {}), ...next.module };
|
|
82
|
+
// Outputs usually live in their own file (#1638) — merge them, or the graph
|
|
83
|
+
// never sees the estate's outputs.tf at all.
|
|
84
|
+
if (next.output) into.output = { ...(into.output ?? {}), ...next.output };
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/** List every `.tf` file directly under `dir` (non-recursive; matches Terraform's own module scoping). */
|
|
@@ -115,6 +115,31 @@ describe("scoreEstate — penalties", () => {
|
|
|
115
115
|
expect(map["aws_sns_topic.alerts"].score).toBe(88); // 100 - 12*1 inbound
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
test("an output block reading a resource costs 4, not 12 (#1638)", () => {
|
|
119
|
+
const tree: Hcl2JsonTree = {
|
|
120
|
+
resource: { aws_s3_bucket: { assets: [{ bucket: "b" }] } },
|
|
121
|
+
output: { assets_bucket: [{ value: "${aws_s3_bucket.assets.bucket}" }] },
|
|
122
|
+
};
|
|
123
|
+
const bucket = byAddress(scoreEstate(buildFixtureGraph(tree)))["aws_s3_bucket.assets"];
|
|
124
|
+
expect(bucket.score).toBe(96); // 100 - 4*1 output
|
|
125
|
+
expect(bucket.breakdown.outputs).toBe(1);
|
|
126
|
+
expect(bucket.breakdown.inbound).toBe(0); // not counted as a data-source patch
|
|
127
|
+
expect(bucket.breakdown.penalties.outputs).toBe(-4);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("an output and a resource reference are counted apart", () => {
|
|
131
|
+
const tree: Hcl2JsonTree = {
|
|
132
|
+
resource: {
|
|
133
|
+
aws_s3_bucket: { assets: [{ bucket: "b" }] },
|
|
134
|
+
aws_lambda_function: { api: [{ environment: { variables: { B: "${aws_s3_bucket.assets.bucket}" } } }] },
|
|
135
|
+
},
|
|
136
|
+
output: { assets_arn: [{ value: "${aws_s3_bucket.assets.arn}" }] },
|
|
137
|
+
};
|
|
138
|
+
const bucket = byAddress(scoreEstate(buildFixtureGraph(tree)))["aws_s3_bucket.assets"];
|
|
139
|
+
expect(bucket.breakdown).toMatchObject({ inbound: 1, outputs: 1 });
|
|
140
|
+
expect(bucket.score).toBe(84); // 100 - 12 - 4
|
|
141
|
+
});
|
|
142
|
+
|
|
118
143
|
test("results are ranked most-peelable first", () => {
|
|
119
144
|
const tree: Hcl2JsonTree = {
|
|
120
145
|
resource: {
|
package/src/terraform/score.ts
CHANGED
|
@@ -6,11 +6,18 @@
|
|
|
6
6
|
* score = 100
|
|
7
7
|
* - 12 * inbound # survivors that depend on this → each a TF data-source patch
|
|
8
8
|
* - 4 * outbound # this depends on survivors → a deferred deploy-time input
|
|
9
|
+
* - 4 * outputs # an output block reads this → a one-line rewrite (#1638)
|
|
9
10
|
* - 15 * (tier - 1) # native-spec map: tier1=0, tier2=-15, tier3=-30
|
|
10
11
|
* - 10 * has_dynamic # count / for_each / data present
|
|
11
12
|
* - 3 * (instances - 1) # state-expanded instance count
|
|
12
13
|
* clamp 0..100 (unsupported provider/type → 0)
|
|
13
14
|
*
|
|
15
|
+
* An `output` block referencing the target is a real inbound dependency — the
|
|
16
|
+
* surviving plan errors on the dangling reference — so it cannot score as
|
|
17
|
+
* free. It is not a data-source patch either: bridging it rewrites one
|
|
18
|
+
* expression in a block that manages no infrastructure, the same order of work
|
|
19
|
+
* as recording an outbound deferred input. Hence 4, not 12.
|
|
20
|
+
*
|
|
14
21
|
* Sub-resources that inline into a parent (see `FOLDS_INTO`) are folded into the
|
|
15
22
|
* parent's carve set: they are removed from the ranking and their edge to the
|
|
16
23
|
* parent is not counted as inbound — inlining them is free, not boundary work.
|
|
@@ -23,8 +30,11 @@ import type { TfGraph, TfNode } from "./types";
|
|
|
23
30
|
export type PeelabilityBand = "clean leaf" | "carvable w/ edits" | "leave in Terraform";
|
|
24
31
|
|
|
25
32
|
export interface PeelabilityBreakdown {
|
|
33
|
+
/** Inbound edges from resource/module blocks — a data-source patch each. */
|
|
26
34
|
inbound: number;
|
|
27
35
|
outbound: number;
|
|
36
|
+
/** Inbound edges from `output` blocks — a one-line rewrite each (#1638). */
|
|
37
|
+
outputs: number;
|
|
28
38
|
tier: 1 | 2 | 3 | null;
|
|
29
39
|
hasDynamic: boolean;
|
|
30
40
|
instances: number;
|
|
@@ -32,6 +42,7 @@ export interface PeelabilityBreakdown {
|
|
|
32
42
|
penalties: {
|
|
33
43
|
inbound: number;
|
|
34
44
|
outbound: number;
|
|
45
|
+
outputs: number;
|
|
35
46
|
tier: number;
|
|
36
47
|
dynamic: number;
|
|
37
48
|
instances: number;
|
|
@@ -82,8 +93,11 @@ function computeFolds(graph: TfGraph): { folded: Set<string>; childrenOf: Map<st
|
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
function scoreNode(node: TfNode, graph: TfGraph, foldedChildren: Set<string>): Peelability {
|
|
85
|
-
// Inbound excludes edges from this node's own folded sub-resources
|
|
86
|
-
|
|
96
|
+
// Inbound excludes edges from this node's own folded sub-resources, and
|
|
97
|
+
// counts output blocks separately — they cost less to bridge (#1638).
|
|
98
|
+
const inboundAll = inboundEdges(graph, node.address).filter((e) => !foldedChildren.has(e.from));
|
|
99
|
+
const inbound = inboundAll.filter((e) => e.fromKind !== "output").length;
|
|
100
|
+
const outputs = inboundAll.length - inbound;
|
|
87
101
|
const outbound = outboundEdges(graph, node.address).length;
|
|
88
102
|
|
|
89
103
|
const tierInfo = node.kind === "module" ? { tier: MODULE_TIER, mapsTo: undefined } : resolveTier(node.type!);
|
|
@@ -99,10 +113,11 @@ function scoreNode(node: TfNode, graph: TfGraph, foldedChildren: Set<string>): P
|
|
|
99
113
|
breakdown: {
|
|
100
114
|
inbound,
|
|
101
115
|
outbound,
|
|
116
|
+
outputs,
|
|
102
117
|
tier: null,
|
|
103
118
|
hasDynamic: node.hasDynamic,
|
|
104
119
|
instances: node.instances,
|
|
105
|
-
penalties: { inbound: 0, outbound: 0, tier: 0, dynamic: 0, instances: 0 },
|
|
120
|
+
penalties: { inbound: 0, outbound: 0, outputs: 0, tier: 0, dynamic: 0, instances: 0 },
|
|
106
121
|
},
|
|
107
122
|
};
|
|
108
123
|
}
|
|
@@ -110,12 +125,19 @@ function scoreNode(node: TfNode, graph: TfGraph, foldedChildren: Set<string>): P
|
|
|
110
125
|
const penalties = {
|
|
111
126
|
inbound: -12 * inbound,
|
|
112
127
|
outbound: -4 * outbound,
|
|
128
|
+
outputs: -4 * outputs,
|
|
113
129
|
tier: -15 * (tier - 1),
|
|
114
130
|
dynamic: node.hasDynamic ? -10 : 0,
|
|
115
131
|
instances: -3 * Math.max(0, node.instances - 1),
|
|
116
132
|
};
|
|
117
133
|
const score = clamp(
|
|
118
|
-
100 +
|
|
134
|
+
100 +
|
|
135
|
+
penalties.inbound +
|
|
136
|
+
penalties.outbound +
|
|
137
|
+
penalties.outputs +
|
|
138
|
+
penalties.tier +
|
|
139
|
+
penalties.dynamic +
|
|
140
|
+
penalties.instances,
|
|
119
141
|
);
|
|
120
142
|
|
|
121
143
|
return {
|
|
@@ -127,6 +149,7 @@ function scoreNode(node: TfNode, graph: TfGraph, foldedChildren: Set<string>): P
|
|
|
127
149
|
breakdown: {
|
|
128
150
|
inbound,
|
|
129
151
|
outbound,
|
|
152
|
+
outputs,
|
|
130
153
|
tier,
|
|
131
154
|
hasDynamic: node.hasDynamic,
|
|
132
155
|
instances: node.instances,
|
package/src/terraform/types.ts
CHANGED
|
@@ -46,6 +46,15 @@ export interface TfNode {
|
|
|
46
46
|
export interface TfEdge {
|
|
47
47
|
from: string;
|
|
48
48
|
to: string;
|
|
49
|
+
/**
|
|
50
|
+
* The kind of block the reference came from, when it is not one of the
|
|
51
|
+
* graph's own nodes. `"output"` marks a root-module `output` block (#1638):
|
|
52
|
+
* a real dependency — `terraform plan` errors on the dangling reference the
|
|
53
|
+
* moment the carve lands — but a cheaper one to bridge than a resource's,
|
|
54
|
+
* since the patch is a single expression edit in a block that manages
|
|
55
|
+
* nothing. Absent means the reference came from a resource or module node.
|
|
56
|
+
*/
|
|
57
|
+
fromKind?: "output";
|
|
49
58
|
/** The attribute path(s) referenced, e.g. `["id", "arn"]`. */
|
|
50
59
|
attrs: string[];
|
|
51
60
|
/**
|
|
@@ -72,5 +81,7 @@ export interface Hcl2JsonTree {
|
|
|
72
81
|
resource?: Record<string, Record<string, unknown[]>>;
|
|
73
82
|
module?: Record<string, unknown[]>;
|
|
74
83
|
data?: Record<string, Record<string, unknown[]>>;
|
|
84
|
+
/** Root-module `output` blocks. Not nodes — they reference, they are not carvable (#1638). */
|
|
85
|
+
output?: Record<string, unknown[]>;
|
|
75
86
|
[k: string]: unknown;
|
|
76
87
|
}
|