@intentius/chant-lexicon-terraform 0.60.0 → 0.61.0
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/describe-resources.d.ts +59 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +124 -0
- package/dist/hcl/edges.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +42 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +6 -0
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/terraform.d.ts +44 -8
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/adoption.d.ts +110 -35
- package/dist/op/adoption.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +92 -58
- package/src/__fixtures__/graph-roots/README.md +21 -0
- package/src/__fixtures__/graph-roots/app/main.tf +41 -0
- package/src/__fixtures__/graph-roots/app/modules/cdn/main.tf +7 -0
- package/src/__fixtures__/graph-roots/network/main.tf +16 -0
- package/src/__fixtures__/live-estate/README.md +65 -19
- package/src/__fixtures__/live-estate/adoptable.tf +13 -0
- package/src/__fixtures__/live-ls.json +5 -5
- package/src/__fixtures__/live-plan.json +67 -35
- package/src/composites/terraform-adopt-op.acceptance.test.ts +36 -50
- package/src/composites/terraform-apply-op.acceptance.test.ts +4 -2
- package/src/describe-resources.live.test.ts +76 -4
- package/src/describe-resources.test.ts +35 -0
- package/src/describe-resources.ts +110 -3
- package/src/hcl/edges.test.ts +207 -0
- package/src/hcl/edges.ts +316 -0
- package/src/hcl/parse.ts +53 -0
- package/src/hcl/roots.ts +14 -0
- package/src/index.ts +1 -0
- package/src/op/activities/choudoufu.test.ts +3 -3
- package/src/op/activities/terraform.ts +70 -21
- package/src/op/adoption.test.ts +129 -11
- package/src/op/adoption.ts +181 -49
package/src/op/adoption.ts
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The adoption ledger: which live resources this estate's markers can claim,
|
|
3
|
-
* read off `live-plan -json`'s document and rendered as text (#2105).
|
|
3
|
+
* read off `live-plan -json`'s document and rendered as text (#2105, #2241).
|
|
4
4
|
*
|
|
5
5
|
* Pure. No filesystem, no child process, no HCL parse — everything here is a
|
|
6
|
-
* projection of
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* projection of two sections of choudoufu's document, which between them state
|
|
7
|
+
* both halves of the answer: the live resource an adoption would claim, and
|
|
8
|
+
* the `tofu-estate`/`tofu-address` pair that claims it. Two tags is the whole
|
|
9
|
+
* ownership contract (choudoufu's `live/MARKERS.md`), so those two values are
|
|
10
|
+
* the whole of what an adoption needs to know.
|
|
11
|
+
*
|
|
12
|
+
* ## The two sections, and why there are two
|
|
13
|
+
*
|
|
14
|
+
* `unowned[]` (choudoufu issue #788) is a live resource read at an identity
|
|
15
|
+
* the configuration itself declares: a log group's name is in the block, so an
|
|
16
|
+
* unmarked live one with that name is found by reading it.
|
|
17
|
+
*
|
|
18
|
+
* `adoptable[]` (choudoufu issue #962, shipped in v0.15.0) is a live resource
|
|
19
|
+
* the estate-wide sweep matched to a declared instance by content, for a
|
|
20
|
+
* declaration that carries no identity at all: EC2 assigns a VPC's id, so a
|
|
21
|
+
* declared `aws_vpc` lands in `omissions[]` as `NEEDS_DISCOVERY` and the live
|
|
22
|
+
* VPC standing at its `cidr_block` is found by comparing arguments. Each row
|
|
23
|
+
* carries `matched[]`, the arguments that agreed, and `adopt_command`, the
|
|
24
|
+
* paste-ready tagging command, so nothing on this path parses the human
|
|
25
|
+
* render any more. Before v0.15.0 the document had no row for that match at
|
|
26
|
+
* all and chant read the two regexes {@link parseAdoptionCommands} still
|
|
27
|
+
* holds, which is what chant #2168 measured and filed.
|
|
28
|
+
*
|
|
29
|
+
* The two sections are disjoint by construction, so this reads both and keys
|
|
30
|
+
* the union by declared address.
|
|
31
|
+
*
|
|
32
|
+
* ## What the empty ledger means
|
|
33
|
+
*
|
|
34
|
+
* `adoptable[]` and `swept[]` are populated only on a run that asked the
|
|
35
|
+
* estate-wide sweep the account-bounded question (`-adoption-only`, or
|
|
36
|
+
* `TOFU_LIVE_COLLECT_UNCLAIMED=1` on a `-json` run). {@link AdoptionLedger}
|
|
37
|
+
* carries `swept` for exactly that reason: an empty `adoptions` under an empty
|
|
38
|
+
* `swept` is "this run did not look", not "there is nothing to adopt", and
|
|
39
|
+
* {@link renderAdoptionLedger} says which.
|
|
12
40
|
*
|
|
13
41
|
* ## Why chant renders a ledger at all
|
|
14
42
|
*
|
|
@@ -18,43 +46,64 @@
|
|
|
18
46
|
* says why each unadoptable instance is unadoptable. But `-adoption-only` and
|
|
19
47
|
* `-json` are refused together (`internal/command/live_plan.go`: "this run
|
|
20
48
|
* cannot produce both reports at once"), and an Op that reports adoptables
|
|
21
|
-
* needs the machine-readable document anyway
|
|
22
|
-
* outcome attributes
|
|
49
|
+
* needs the machine-readable document anyway, for the counts it publishes as
|
|
50
|
+
* outcome attributes and for the addresses an adoption step acts on. So the
|
|
23
51
|
* ledger below is rendered from the document that run already has, in the row
|
|
24
52
|
* form `-adoption-only` prints, rather than paying for a third live read.
|
|
25
53
|
*
|
|
26
54
|
* ## Ambiguity
|
|
27
55
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* two entries carrying the same `addr`. That is the ambiguous
|
|
31
|
-
* tag write claims the address, and picking one of the two is
|
|
32
|
-
* the estate rather than something a tool infers.
|
|
33
|
-
* separates those into `contested` and never lets
|
|
34
|
-
* `TerraformAdoptOp` passes the contested list to
|
|
35
|
-
* result names what it refused as well as what it
|
|
56
|
+
* Both sections are one entry per live resource, keyed by the declared
|
|
57
|
+
* instance the resource was matched to, so two live resources at one
|
|
58
|
+
* declaration are two entries carrying the same `addr`. That is the ambiguous
|
|
59
|
+
* case: no single tag write claims the address, and picking one of the two is
|
|
60
|
+
* a decision about the estate rather than something a tool infers.
|
|
61
|
+
* {@link readAdoptionLedger} separates those into `contested` and never lets
|
|
62
|
+
* them into `adoptions`, and `TerraformAdoptOp` passes the contested list to
|
|
63
|
+
* its Adopt step so the Op's result names what it refused as well as what it
|
|
64
|
+
* wrote.
|
|
36
65
|
*/
|
|
37
66
|
|
|
67
|
+
/** One argument a content match rested on, as choudoufu's `adoptable[].matched[]`. */
|
|
68
|
+
export interface AdoptionMatch {
|
|
69
|
+
/** The argument's name in the declared block, `cidr_block` and the like. */
|
|
70
|
+
attribute: string;
|
|
71
|
+
/** The value both the declaration and the live resource carried. */
|
|
72
|
+
value: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
/** One live resource a marker write would bind to a declared instance. */
|
|
39
76
|
export interface AdoptionCandidate {
|
|
40
|
-
/** The declared instance address
|
|
77
|
+
/** The declared instance address the live resource was matched to. */
|
|
41
78
|
addr: string;
|
|
42
|
-
/** The live resource's type, as choudoufu's `
|
|
79
|
+
/** The live resource's type, as choudoufu's `type`. */
|
|
43
80
|
type: string;
|
|
44
|
-
/** The identity the live resource was read with
|
|
81
|
+
/** The identity the live resource was read with: the handle a human, or a tagging call, needs. */
|
|
45
82
|
identity: string;
|
|
46
83
|
/** The `tofu-estate` value that adopts it. */
|
|
47
84
|
markerEstate: string;
|
|
48
85
|
/** The `tofu-address` value that adopts it, escaped as choudoufu stores it. */
|
|
49
86
|
markerAddress: string;
|
|
50
87
|
/**
|
|
51
|
-
* The paste-ready tagging command choudoufu printed for this address
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
88
|
+
* The paste-ready tagging command choudoufu printed for this address, when
|
|
89
|
+
* it printed one. Absent for a type whose service has its own tagging call
|
|
90
|
+
* this fork does not spell out (IAM, Route53, S3 and friends): the two
|
|
91
|
+
* marker values above are still the whole contract, but the caller has to
|
|
92
|
+
* write them itself.
|
|
93
|
+
*
|
|
94
|
+
* An `adoptable[]` row carries its own (`adopt_command`, choudoufu #962). An
|
|
95
|
+
* `unowned[]` row does not, so a command for one of those comes from the
|
|
96
|
+
* `commands` map {@link parseAdoptionCommands} builds off the human render.
|
|
56
97
|
*/
|
|
57
98
|
command?: string;
|
|
99
|
+
/**
|
|
100
|
+
* The arguments the declaration and the live resource agreed on exactly, in
|
|
101
|
+
* the order choudoufu's matcher compared them. Present on a content match
|
|
102
|
+
* (`adoptable[]`) and absent on a row found by reading a declared identity
|
|
103
|
+
* (`unowned[]`), which matched on the identity itself and has nothing else
|
|
104
|
+
* to name.
|
|
105
|
+
*/
|
|
106
|
+
matched?: AdoptionMatch[];
|
|
58
107
|
}
|
|
59
108
|
|
|
60
109
|
/** What {@link readAdoptionLedger} found in one `live-plan -json` document. */
|
|
@@ -70,6 +119,13 @@ export interface AdoptionLedger {
|
|
|
70
119
|
contested: AdoptionCandidate[];
|
|
71
120
|
/** How many distinct declared addresses are contested. */
|
|
72
121
|
ambiguous: number;
|
|
122
|
+
/**
|
|
123
|
+
* `swept[]`: every resource type the estate-wide sweep listed in full on the
|
|
124
|
+
* run that produced the document. Empty means the run never asked the
|
|
125
|
+
* account-bounded question, so an empty `adoptions` beside it is silence
|
|
126
|
+
* rather than a finding.
|
|
127
|
+
*/
|
|
128
|
+
swept: string[];
|
|
73
129
|
}
|
|
74
130
|
|
|
75
131
|
/** `unowned[]`'s entry shape, per choudoufu's `views.StatelessUnowned` json tags. */
|
|
@@ -82,45 +138,93 @@ interface UnownedEntry {
|
|
|
82
138
|
adopt_tofu_address?: unknown;
|
|
83
139
|
}
|
|
84
140
|
|
|
141
|
+
/** `adoptable[]`'s entry shape, per choudoufu's `views.LivePlanAdoptable` json tags. */
|
|
142
|
+
interface AdoptableEntry extends UnownedEntry {
|
|
143
|
+
matched?: unknown;
|
|
144
|
+
adopt_command?: unknown;
|
|
145
|
+
}
|
|
146
|
+
|
|
85
147
|
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
86
148
|
|
|
149
|
+
/** `matched[]` as choudoufu writes it, dropping anything that is not a pair of strings. */
|
|
150
|
+
function readMatched(raw: unknown): AdoptionMatch[] {
|
|
151
|
+
if (!Array.isArray(raw)) return [];
|
|
152
|
+
const out: AdoptionMatch[] = [];
|
|
153
|
+
for (const item of raw) {
|
|
154
|
+
const m = (item ?? {}) as { attribute?: unknown; value?: unknown };
|
|
155
|
+
const attribute = str(m.attribute);
|
|
156
|
+
if (attribute) out.push({ attribute, value: str(m.value) });
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** `swept[]` as choudoufu writes it: resource type names, and nothing else. */
|
|
162
|
+
function readSwept(document: unknown): string[] {
|
|
163
|
+
const swept = (document as { swept?: unknown } | null | undefined)?.swept;
|
|
164
|
+
return Array.isArray(swept) ? swept.filter((t): t is string => typeof t === "string") : [];
|
|
165
|
+
}
|
|
166
|
+
|
|
87
167
|
/**
|
|
88
|
-
* Project a `live-plan -json` document's `unowned`
|
|
89
|
-
* and the contested sets.
|
|
168
|
+
* Project a `live-plan -json` document's `unowned` and `adoptable` sections
|
|
169
|
+
* into the adoptable and the contested sets.
|
|
90
170
|
*
|
|
91
171
|
* An entry counts as a candidate only when choudoufu offered both marker
|
|
92
|
-
* values. Both empty means adoption was not this run's to offer
|
|
172
|
+
* values. Both empty means adoption was not this run's to offer: the resource
|
|
93
173
|
* belongs to another estate (`tofu_estate` names it), or the run had no estate
|
|
94
|
-
* name of its own
|
|
174
|
+
* name of its own, and such an entry is neither adoptable nor contested here,
|
|
95
175
|
* because there is no tag write to refuse.
|
|
96
176
|
*
|
|
97
177
|
* `commands` maps a declared address to the paste-ready tagging command
|
|
98
|
-
* choudoufu printed for it, from {@link parseAdoptionCommands}
|
|
99
|
-
*
|
|
178
|
+
* choudoufu printed for it, from {@link parseAdoptionCommands}. An
|
|
179
|
+
* `adoptable[]` row carries its own command in the document and never needs
|
|
180
|
+
* the map; omit the map entirely when the run had no `-adoption-only` render
|
|
181
|
+
* to read one out of.
|
|
100
182
|
*/
|
|
101
183
|
export function readAdoptionLedger(document: unknown, commands?: ReadonlyMap<string, string>): AdoptionLedger {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
184
|
+
const doc = (document ?? {}) as { unowned?: unknown; adoptable?: unknown };
|
|
185
|
+
const swept = readSwept(document);
|
|
105
186
|
const byAddr = new Map<string, AdoptionCandidate[]>();
|
|
106
|
-
|
|
187
|
+
|
|
188
|
+
const add = (candidate: AdoptionCandidate): void => {
|
|
189
|
+
const at = byAddr.get(candidate.addr);
|
|
190
|
+
if (at) at.push(candidate);
|
|
191
|
+
else byAddr.set(candidate.addr, [candidate]);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
for (const raw of Array.isArray(doc.unowned) ? doc.unowned : []) {
|
|
107
195
|
const e = (raw ?? {}) as UnownedEntry;
|
|
108
196
|
const markerEstate = str(e.adopt_tofu_estate);
|
|
109
197
|
const markerAddress = str(e.adopt_tofu_address);
|
|
110
198
|
if (!markerEstate && !markerAddress) continue;
|
|
111
199
|
const addr = str(e.addr);
|
|
112
200
|
const command = commands?.get(addr);
|
|
113
|
-
|
|
201
|
+
add({
|
|
114
202
|
addr,
|
|
115
203
|
type: str(e.type),
|
|
116
204
|
identity: str(e.identity),
|
|
117
205
|
markerEstate,
|
|
118
206
|
markerAddress,
|
|
119
207
|
...(command ? { command } : {}),
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const raw of Array.isArray(doc.adoptable) ? doc.adoptable : []) {
|
|
212
|
+
const e = (raw ?? {}) as AdoptableEntry;
|
|
213
|
+
const markerEstate = str(e.adopt_tofu_estate);
|
|
214
|
+
const markerAddress = str(e.adopt_tofu_address);
|
|
215
|
+
if (!markerEstate && !markerAddress) continue;
|
|
216
|
+
const addr = str(e.addr);
|
|
217
|
+
const command = str(e.adopt_command) || commands?.get(addr);
|
|
218
|
+
const matched = readMatched(e.matched);
|
|
219
|
+
add({
|
|
220
|
+
addr,
|
|
221
|
+
type: str(e.type),
|
|
222
|
+
identity: str(e.identity),
|
|
223
|
+
markerEstate,
|
|
224
|
+
markerAddress,
|
|
225
|
+
...(command ? { command } : {}),
|
|
226
|
+
...(matched.length > 0 ? { matched } : {}),
|
|
227
|
+
});
|
|
124
228
|
}
|
|
125
229
|
|
|
126
230
|
const adoptions: AdoptionCandidate[] = [];
|
|
@@ -134,7 +238,7 @@ export function readAdoptionLedger(document: unknown, commands?: ReadonlyMap<str
|
|
|
134
238
|
contested.push(...candidates);
|
|
135
239
|
}
|
|
136
240
|
}
|
|
137
|
-
return { adoptions, contested, ambiguous };
|
|
241
|
+
return { adoptions, contested, ambiguous, swept };
|
|
138
242
|
}
|
|
139
243
|
|
|
140
244
|
/**
|
|
@@ -153,6 +257,17 @@ export function readAdoptionLedger(document: unknown, commands?: ReadonlyMap<str
|
|
|
153
257
|
* choudoufu already wrote.
|
|
154
258
|
*
|
|
155
259
|
* An address the render offered no command for is simply absent from the map.
|
|
260
|
+
*
|
|
261
|
+
* ## Which caller still needs this (#2241)
|
|
262
|
+
*
|
|
263
|
+
* One: the `unowned[]` half of {@link readAdoptionLedger}. choudoufu's
|
|
264
|
+
* `views.StatelessUnowned` has no command field, so a live resource found at
|
|
265
|
+
* an identity the configuration declares still gets its paste-ready command
|
|
266
|
+
* from the human render and from nowhere else. The `adoptable[]` half no
|
|
267
|
+
* longer reads a line of text: those rows carry `adopt_command` in the
|
|
268
|
+
* document itself since v0.15.0 (choudoufu #962), which is what made the
|
|
269
|
+
* adopt path stop resting on two regexes over a render nobody promised to
|
|
270
|
+
* keep stable.
|
|
156
271
|
*/
|
|
157
272
|
export function parseAdoptionCommands(ledgerText: string): Map<string, string> {
|
|
158
273
|
const commands = new Map<string, string>();
|
|
@@ -185,10 +300,17 @@ const plural = (n: number, one: string, many: string): string => (n === 1 ? one
|
|
|
185
300
|
* One line per adoptable match, naming the declared address, the live resource
|
|
186
301
|
* it binds, and the two tag values that adopt it, in the token forms
|
|
187
302
|
* `-adoption-only` prints them in (`<addr> <- <type> <identity>`,
|
|
188
|
-
* `tofu-estate=`, `tofu-address=`).
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
303
|
+
* `tofu-estate=`, `tofu-address=`). A content match adds the arguments it
|
|
304
|
+
* rested on under its row, in the same `matched on:` form the human render
|
|
305
|
+
* uses, because "these two agreed on this cidr" is the whole evidence for a
|
|
306
|
+
* match nobody read an identity for. Contested addresses follow, listed and
|
|
307
|
+
* not offered.
|
|
308
|
+
*
|
|
309
|
+
* An empty ledger still renders a line: "nothing adoptable" is a result a
|
|
310
|
+
* reader wants, and a section that vanishes reads as one that was never
|
|
311
|
+
* computed. Which of the two empties it is comes from `swept`: a run that
|
|
312
|
+
* asked no estate-wide sweep found nothing because it did not look, and
|
|
313
|
+
* saying so is the difference between a report and a silence.
|
|
192
314
|
*/
|
|
193
315
|
export function renderAdoptionLedger(ledger: AdoptionLedger, estate?: string): string {
|
|
194
316
|
const where = estate ? `, estate ${JSON.stringify(estate)}` : "";
|
|
@@ -197,8 +319,14 @@ export function renderAdoptionLedger(ledger: AdoptionLedger, estate?: string): s
|
|
|
197
319
|
if (ledger.adoptions.length === 0) {
|
|
198
320
|
lines.push(`Adoptable now: nothing${where}`, "");
|
|
199
321
|
lines.push(
|
|
200
|
-
|
|
201
|
-
"
|
|
322
|
+
ledger.swept.length === 0
|
|
323
|
+
? "No estate-wide sweep ran on this plan, so nothing here says whether an unmarked live " +
|
|
324
|
+
"resource exists for any declared instance. This is silence, not a finding: ask the " +
|
|
325
|
+
"account-bounded question with an adoption run."
|
|
326
|
+
: "No live resource this run could claim was found, across " +
|
|
327
|
+
`${ledger.swept.length} swept resource ${plural(ledger.swept.length, "type", "types")} ` +
|
|
328
|
+
`(${ledger.swept.join(", ")}). Anything the plan proposes creating, it proposes ` +
|
|
329
|
+
"creating for real.",
|
|
202
330
|
);
|
|
203
331
|
} else {
|
|
204
332
|
lines.push(
|
|
@@ -206,8 +334,9 @@ export function renderAdoptionLedger(ledger: AdoptionLedger, estate?: string): s
|
|
|
206
334
|
"",
|
|
207
335
|
);
|
|
208
336
|
lines.push(
|
|
209
|
-
"Each line is a live resource found at a declared resource's
|
|
210
|
-
"
|
|
337
|
+
"Each line is a live resource this estate could claim, found either at a declared resource's " +
|
|
338
|
+
"own identity or by matching a declaration's arguments, and carrying no marker for this " +
|
|
339
|
+
"estate. Writing the two tags shown adopts it; nothing is bound until they are written, " +
|
|
211
340
|
"because ownership is the tofu-estate and tofu-address pair and nothing else.",
|
|
212
341
|
"",
|
|
213
342
|
);
|
|
@@ -216,6 +345,9 @@ export function renderAdoptionLedger(ledger: AdoptionLedger, estate?: string): s
|
|
|
216
345
|
` ${c.addr} <- ${c.type} ${c.identity} write: ` +
|
|
217
346
|
`tofu-estate=${c.markerEstate} tofu-address=${c.markerAddress}`,
|
|
218
347
|
);
|
|
348
|
+
if (c.matched?.length) {
|
|
349
|
+
lines.push(` matched on: ${c.matched.map((m) => `${m.attribute}=${m.value}`).join(", ")}`);
|
|
350
|
+
}
|
|
219
351
|
}
|
|
220
352
|
}
|
|
221
353
|
|