@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,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the argument of a `jsonencode(...)` call as structure, when it is
|
|
3
|
+
* literal (chant #2286, shared with #2287).
|
|
4
|
+
*
|
|
5
|
+
* hcl2json does not evaluate expressions. `policy = jsonencode({...})` and
|
|
6
|
+
* `container_definitions = jsonencode([...])` both reach a block body as ONE
|
|
7
|
+
* string holding the expression's source text, wrapped as an interpolation:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* "${jsonencode({\n Version = \"2012-10-17\"\n Statement = [{ Action = \"*\" }]\n })}"
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The text inside is HCL object syntax, not JSON (`=` or `:` separators, bare
|
|
14
|
+
* keys, newline-separated items, `#` comments). A rule that wants the policy
|
|
15
|
+
* or the container definitions has to read that text itself. This file does
|
|
16
|
+
* so, narrowly: it accepts literal objects, tuples, quoted strings, numbers,
|
|
17
|
+
* `true`, `false` and `null`, and nothing else. Any expression in the
|
|
18
|
+
* argument (a reference, a function call, a for-expression, a splat, a
|
|
19
|
+
* conditional, an operator, a template interpolation inside a string, a
|
|
20
|
+
* heredoc, a parenthesised key) makes the whole read `not-determined`, with a
|
|
21
|
+
* reason naming what was found. It never evaluates anything, so it can never
|
|
22
|
+
* return a value Terraform would not also produce from the same text.
|
|
23
|
+
*
|
|
24
|
+
* Three answers:
|
|
25
|
+
* - `literal`: the value is a single whole-string `${jsonencode(...)}` and
|
|
26
|
+
* its argument is fully literal. `value` is the plain JS value
|
|
27
|
+
* `jsonencode` would encode.
|
|
28
|
+
* - `not-determined`: the value is a `${jsonencode(...)}` call, but the
|
|
29
|
+
* argument holds something this reader does not evaluate.
|
|
30
|
+
* - `not-jsonencode`: the value is not a `${jsonencode(...)}` call at all
|
|
31
|
+
* (a plain string, a reference, `file(...)`, a number, absent). Callers
|
|
32
|
+
* decide what that means for their attribute; a plain string, for example,
|
|
33
|
+
* may itself be JSON from a heredoc.
|
|
34
|
+
*
|
|
35
|
+
* The whole-string test is shape-based: the string starts `${jsonencode(` and
|
|
36
|
+
* ends `)}`. A string that matches that shape but is really a template with
|
|
37
|
+
* two interpolations (`${jsonencode(a)}-${jsonencode(b)}`) fails the literal
|
|
38
|
+
* parse at the point where the first call closes early, and reads as
|
|
39
|
+
* `not-determined`, never as `literal`.
|
|
40
|
+
*/
|
|
41
|
+
/** Marks a value position `readJsonencode(raw, { unknownLeaves: true })` could not read. */
|
|
42
|
+
export const UNKNOWN = Symbol("jsonencode.unknown");
|
|
43
|
+
/**
|
|
44
|
+
* Marks an object, read with `unknownLeaves`, that had members whose KEY is an
|
|
45
|
+
* expression (`"router-${var.n}.rule" = ...`, `(var.k) = ...`). Those members
|
|
46
|
+
* are dropped from the object and their reasons listed here, so a reader can
|
|
47
|
+
* tell "this key is absent" from "some key here could not be read".
|
|
48
|
+
*/
|
|
49
|
+
export const UNKNOWN_KEYS = Symbol("jsonencode.unknownKeys");
|
|
50
|
+
/** Whether `obj` is an object that dropped one or more members with an expression key. */
|
|
51
|
+
export function hasUnknownKeys(obj) {
|
|
52
|
+
return (typeof obj === "object" &&
|
|
53
|
+
obj !== null &&
|
|
54
|
+
Array.isArray(obj[UNKNOWN_KEYS]) &&
|
|
55
|
+
obj[UNKNOWN_KEYS].length > 0);
|
|
56
|
+
}
|
|
57
|
+
/** Whether `v` is a value position the reader skipped. */
|
|
58
|
+
export function isUnknown(v) {
|
|
59
|
+
return typeof v === "object" && v !== null && v[UNKNOWN] === true;
|
|
60
|
+
}
|
|
61
|
+
const PREFIX = /^\$\{\s*jsonencode\s*\(/;
|
|
62
|
+
const SUFFIX = /\)\s*\}$/;
|
|
63
|
+
/** Read `raw` (an hcl2json attribute value) as a `jsonencode(...)` call. */
|
|
64
|
+
export function readJsonencode(raw, options = {}) {
|
|
65
|
+
if (typeof raw !== "string")
|
|
66
|
+
return { kind: "not-jsonencode" };
|
|
67
|
+
const prefix = PREFIX.exec(raw);
|
|
68
|
+
if (!prefix || !SUFFIX.test(raw))
|
|
69
|
+
return { kind: "not-jsonencode" };
|
|
70
|
+
const reader = new Reader(raw, prefix[0].length, options.unknownLeaves === true);
|
|
71
|
+
try {
|
|
72
|
+
const value = reader.value(0);
|
|
73
|
+
reader.skipSpace();
|
|
74
|
+
// The argument must end at the call's own `)`, then the interpolation's `}`.
|
|
75
|
+
reader.expect(")", "a second argument or an expression after the literal");
|
|
76
|
+
reader.skipSpace();
|
|
77
|
+
reader.expect("}", "text after the jsonencode call");
|
|
78
|
+
if (!reader.atEnd())
|
|
79
|
+
throw new NotLiteral("text after the jsonencode call");
|
|
80
|
+
return { kind: "literal", value };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err instanceof NotLiteral)
|
|
84
|
+
return { kind: "not-determined", reason: err.message };
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class NotLiteral extends Error {
|
|
89
|
+
}
|
|
90
|
+
/** Damage to the shape itself: never recovered by skipping a leaf. */
|
|
91
|
+
class Structural extends NotLiteral {
|
|
92
|
+
}
|
|
93
|
+
/** An object key that is an expression; recoverable by dropping the member under `unknownLeaves`. */
|
|
94
|
+
class KeyNotLiteral extends Structural {
|
|
95
|
+
}
|
|
96
|
+
const IDENT = /[A-Za-z_][A-Za-z0-9_-]*/y;
|
|
97
|
+
const NUMBER = /-?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
98
|
+
/** A cursor over the source text of the jsonencode argument. */
|
|
99
|
+
class Reader {
|
|
100
|
+
constructor(src, pos, unknownLeaves = false) {
|
|
101
|
+
this.src = src;
|
|
102
|
+
this.pos = pos;
|
|
103
|
+
this.unknownLeaves = unknownLeaves;
|
|
104
|
+
}
|
|
105
|
+
atEnd() {
|
|
106
|
+
return this.pos >= this.src.length;
|
|
107
|
+
}
|
|
108
|
+
peek() {
|
|
109
|
+
return this.src[this.pos] ?? "";
|
|
110
|
+
}
|
|
111
|
+
expect(ch, what) {
|
|
112
|
+
if (this.peek() !== ch)
|
|
113
|
+
throw new NotLiteral(what === "" ? `expected "${ch}"` : what);
|
|
114
|
+
this.pos++;
|
|
115
|
+
}
|
|
116
|
+
/** Whitespace, newlines and the three HCL comment forms. */
|
|
117
|
+
skipSpace(newlines = true) {
|
|
118
|
+
for (;;) {
|
|
119
|
+
const c = this.peek();
|
|
120
|
+
if (c === " " || c === "\t" || c === "\r" || (newlines && c === "\n")) {
|
|
121
|
+
this.pos++;
|
|
122
|
+
}
|
|
123
|
+
else if (c === "#" || (c === "/" && this.src[this.pos + 1] === "/")) {
|
|
124
|
+
const end = this.src.indexOf("\n", this.pos);
|
|
125
|
+
this.pos = end === -1 ? this.src.length : end;
|
|
126
|
+
}
|
|
127
|
+
else if (c === "/" && this.src[this.pos + 1] === "*") {
|
|
128
|
+
const end = this.src.indexOf("*/", this.pos + 2);
|
|
129
|
+
if (end === -1)
|
|
130
|
+
throw new NotLiteral("an unterminated comment");
|
|
131
|
+
this.pos = end + 2;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** One value. `depth` is 0 for the whole argument, which is never skipped. */
|
|
139
|
+
value(depth) {
|
|
140
|
+
this.skipSpace();
|
|
141
|
+
if (!this.unknownLeaves || depth === 0)
|
|
142
|
+
return this.literal(depth);
|
|
143
|
+
const start = this.pos;
|
|
144
|
+
try {
|
|
145
|
+
return this.literal(depth);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (!(err instanceof NotLiteral) || err instanceof Structural)
|
|
149
|
+
throw err;
|
|
150
|
+
this.pos = start;
|
|
151
|
+
this.skipExpression();
|
|
152
|
+
return { [UNKNOWN]: true, reason: err.message, source: this.src.slice(start, this.pos).trim() };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
literal(depth) {
|
|
156
|
+
const c = this.peek();
|
|
157
|
+
let v;
|
|
158
|
+
if (c === "{")
|
|
159
|
+
v = this.object(depth + 1);
|
|
160
|
+
else if (c === "[")
|
|
161
|
+
v = this.tuple(depth + 1);
|
|
162
|
+
else if (c === '"')
|
|
163
|
+
v = this.string();
|
|
164
|
+
else if (c === "<" && this.src.startsWith("<<", this.pos))
|
|
165
|
+
throw new NotLiteral("a heredoc");
|
|
166
|
+
else if (c === "(")
|
|
167
|
+
throw new NotLiteral("a parenthesised expression");
|
|
168
|
+
else if (c === "-" || (c >= "0" && c <= "9"))
|
|
169
|
+
v = this.number();
|
|
170
|
+
else
|
|
171
|
+
v = this.keyword();
|
|
172
|
+
this.afterValue();
|
|
173
|
+
return v;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A literal is complete only if what follows it closes or separates. An
|
|
177
|
+
* operator, a conditional, an attribute access or an index after it makes
|
|
178
|
+
* the whole thing an expression.
|
|
179
|
+
*/
|
|
180
|
+
afterValue() {
|
|
181
|
+
this.skipSpace(false);
|
|
182
|
+
const c = this.peek();
|
|
183
|
+
if (c === "" || c === "\n" || c === "," || c === "}" || c === "]" || c === ")")
|
|
184
|
+
return;
|
|
185
|
+
const next = this.src[this.pos + 1];
|
|
186
|
+
if (c === "#" || (c === "/" && (next === "/" || next === "*")))
|
|
187
|
+
return; // a comment, consumed by the caller's skipSpace
|
|
188
|
+
if (c === "?")
|
|
189
|
+
throw new NotLiteral("a conditional expression");
|
|
190
|
+
if (c === "." || c === "[")
|
|
191
|
+
throw new NotLiteral("an attribute access, index or splat");
|
|
192
|
+
throw new NotLiteral(`an operator or expression ("${c}")`);
|
|
193
|
+
}
|
|
194
|
+
object(depth) {
|
|
195
|
+
this.expect("{", "");
|
|
196
|
+
const out = {};
|
|
197
|
+
for (;;) {
|
|
198
|
+
this.skipSpace();
|
|
199
|
+
if (this.peek() === "}") {
|
|
200
|
+
this.pos++;
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
const quoted = this.peek() === '"';
|
|
204
|
+
const memberStart = this.pos;
|
|
205
|
+
let key;
|
|
206
|
+
try {
|
|
207
|
+
key = this.key();
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
if (!(err instanceof KeyNotLiteral) || !this.unknownLeaves)
|
|
211
|
+
throw err;
|
|
212
|
+
// Drop the whole member: the skip runs from the key through its value
|
|
213
|
+
// to the separator, balancing brackets and quotes on the way.
|
|
214
|
+
this.pos = memberStart;
|
|
215
|
+
this.skipExpression();
|
|
216
|
+
const bag = out;
|
|
217
|
+
(bag[UNKNOWN_KEYS] ?? (bag[UNKNOWN_KEYS] = [])).push(err.message);
|
|
218
|
+
this.skipSpace(false);
|
|
219
|
+
if (this.peek() === ",")
|
|
220
|
+
this.pos++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
this.skipSpace(false);
|
|
224
|
+
const sep = this.peek();
|
|
225
|
+
if (sep !== "=" && sep !== ":") {
|
|
226
|
+
// A for-expression is a VALUE: under unknownLeaves the enclosing
|
|
227
|
+
// value() skips it as a leaf; as the whole argument it is not determined.
|
|
228
|
+
if (!quoted && key === "for")
|
|
229
|
+
throw new NotLiteral("a for-expression");
|
|
230
|
+
throw new Structural(`an expression where "${key}" needs = or :`);
|
|
231
|
+
}
|
|
232
|
+
this.pos++;
|
|
233
|
+
out[key] = this.value(depth);
|
|
234
|
+
this.skipSpace(false);
|
|
235
|
+
if (this.peek() === ",")
|
|
236
|
+
this.pos++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
key() {
|
|
240
|
+
const c = this.peek();
|
|
241
|
+
if (c === '"') {
|
|
242
|
+
try {
|
|
243
|
+
return this.string();
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (err instanceof NotLiteral)
|
|
247
|
+
throw new KeyNotLiteral(`an object key holding ${err.message.replace(/^a string with /, "")}`);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (c === "(")
|
|
252
|
+
throw new KeyNotLiteral("a computed object key");
|
|
253
|
+
IDENT.lastIndex = this.pos;
|
|
254
|
+
const m = IDENT.exec(this.src);
|
|
255
|
+
if (!m)
|
|
256
|
+
throw new Structural(`an unexpected "${c}" where an object key belongs`);
|
|
257
|
+
this.pos += m[0].length;
|
|
258
|
+
return m[0];
|
|
259
|
+
}
|
|
260
|
+
tuple(depth) {
|
|
261
|
+
this.expect("[", "");
|
|
262
|
+
this.skipSpace();
|
|
263
|
+
if (/^for\s/.test(this.src.slice(this.pos, this.pos + 4)))
|
|
264
|
+
throw new NotLiteral("a for-expression");
|
|
265
|
+
const out = [];
|
|
266
|
+
for (;;) {
|
|
267
|
+
this.skipSpace();
|
|
268
|
+
if (this.peek() === "]") {
|
|
269
|
+
this.pos++;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
out.push(this.value(depth));
|
|
273
|
+
this.skipSpace();
|
|
274
|
+
if (this.peek() === ",")
|
|
275
|
+
this.pos++;
|
|
276
|
+
else if (this.peek() !== "]")
|
|
277
|
+
throw new NotLiteral("a tuple item followed by an expression");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** A quoted HCL string. A `${` or `%{` (not escaped as `$${`/`%%{`) is a template. */
|
|
281
|
+
string() {
|
|
282
|
+
this.expect('"', "");
|
|
283
|
+
let out = "";
|
|
284
|
+
for (;;) {
|
|
285
|
+
const c = this.peek();
|
|
286
|
+
if (c === "")
|
|
287
|
+
throw new NotLiteral("an unterminated string");
|
|
288
|
+
if (c === "\n")
|
|
289
|
+
throw new NotLiteral("a newline inside a quoted string");
|
|
290
|
+
if (c === '"') {
|
|
291
|
+
this.pos++;
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
if ((c === "$" || c === "%") && this.src[this.pos + 1] === c && this.src[this.pos + 2] === "{") {
|
|
295
|
+
out += `${c}{`;
|
|
296
|
+
this.pos += 3;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
|
|
300
|
+
throw new NotLiteral(c === "$" ? "a string with a ${...} interpolation" : "a string with a %{...} directive");
|
|
301
|
+
}
|
|
302
|
+
if (c === "\\") {
|
|
303
|
+
out += this.escape();
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
out += c;
|
|
307
|
+
this.pos++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
escape() {
|
|
311
|
+
const e = this.src[this.pos + 1] ?? "";
|
|
312
|
+
this.pos += 2;
|
|
313
|
+
switch (e) {
|
|
314
|
+
case "n":
|
|
315
|
+
return "\n";
|
|
316
|
+
case "r":
|
|
317
|
+
return "\r";
|
|
318
|
+
case "t":
|
|
319
|
+
return "\t";
|
|
320
|
+
case '"':
|
|
321
|
+
return '"';
|
|
322
|
+
case "\\":
|
|
323
|
+
return "\\";
|
|
324
|
+
case "u":
|
|
325
|
+
case "U": {
|
|
326
|
+
const len = e === "u" ? 4 : 8;
|
|
327
|
+
const hex = this.src.slice(this.pos, this.pos + len);
|
|
328
|
+
if (!/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== len)
|
|
329
|
+
throw new NotLiteral("a malformed unicode escape");
|
|
330
|
+
this.pos += len;
|
|
331
|
+
return String.fromCodePoint(parseInt(hex, 16));
|
|
332
|
+
}
|
|
333
|
+
default:
|
|
334
|
+
throw new NotLiteral(`an unknown string escape "\\${e}"`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
number() {
|
|
338
|
+
NUMBER.lastIndex = this.pos;
|
|
339
|
+
const m = NUMBER.exec(this.src);
|
|
340
|
+
if (!m)
|
|
341
|
+
throw new NotLiteral("a unary operator on an expression");
|
|
342
|
+
this.pos += m[0].length;
|
|
343
|
+
return Number(m[0]);
|
|
344
|
+
}
|
|
345
|
+
keyword() {
|
|
346
|
+
IDENT.lastIndex = this.pos;
|
|
347
|
+
const m = IDENT.exec(this.src);
|
|
348
|
+
if (!m)
|
|
349
|
+
throw new NotLiteral(`an unexpected "${this.peek()}"`);
|
|
350
|
+
const word = m[0];
|
|
351
|
+
this.pos += word.length;
|
|
352
|
+
if (word === "true")
|
|
353
|
+
return true;
|
|
354
|
+
if (word === "false")
|
|
355
|
+
return false;
|
|
356
|
+
if (word === "null")
|
|
357
|
+
return null;
|
|
358
|
+
this.skipSpace(false);
|
|
359
|
+
if (this.peek() === "(")
|
|
360
|
+
throw new NotLiteral(`a function call (${word}(...))`);
|
|
361
|
+
if (word === "for")
|
|
362
|
+
throw new NotLiteral("a for-expression");
|
|
363
|
+
return this.reference(word);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Skip one value expression from the cursor: balance `()[]{}` and quoted
|
|
367
|
+
* strings (with their `${...}` interpolations), stop at a `,`, a newline, a
|
|
368
|
+
* comment or a closing bracket at depth 0. A heredoc is skipped to its
|
|
369
|
+
* closing marker line.
|
|
370
|
+
*/
|
|
371
|
+
skipExpression() {
|
|
372
|
+
let depth = 0;
|
|
373
|
+
while (!this.atEnd()) {
|
|
374
|
+
const c = this.peek();
|
|
375
|
+
if (depth === 0) {
|
|
376
|
+
if (c === "," || c === "\n" || c === "}" || c === "]" || c === ")" || c === "#")
|
|
377
|
+
return;
|
|
378
|
+
if (c === "/" && (this.src[this.pos + 1] === "/" || this.src[this.pos + 1] === "*"))
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (c === '"')
|
|
382
|
+
this.skipQuoted();
|
|
383
|
+
else if (c === "<" && this.src.startsWith("<<", this.pos))
|
|
384
|
+
this.skipHeredoc();
|
|
385
|
+
else {
|
|
386
|
+
if (c === "(" || c === "[" || c === "{")
|
|
387
|
+
depth++;
|
|
388
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
389
|
+
depth--;
|
|
390
|
+
this.pos++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Skip a quoted string, including any `${...}` interpolations (which may hold strings). */
|
|
395
|
+
skipQuoted() {
|
|
396
|
+
this.pos++;
|
|
397
|
+
while (!this.atEnd()) {
|
|
398
|
+
const c = this.peek();
|
|
399
|
+
if (c === "\\")
|
|
400
|
+
this.pos += 2;
|
|
401
|
+
else if (c === '"') {
|
|
402
|
+
this.pos++;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
else if ((c === "$" || c === "%") && this.src[this.pos + 1] === "{") {
|
|
406
|
+
this.pos += 2;
|
|
407
|
+
let depth = 1;
|
|
408
|
+
while (!this.atEnd() && depth > 0) {
|
|
409
|
+
const d = this.peek();
|
|
410
|
+
if (d === '"') {
|
|
411
|
+
this.skipQuoted();
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (d === "{")
|
|
415
|
+
depth++;
|
|
416
|
+
else if (d === "}")
|
|
417
|
+
depth--;
|
|
418
|
+
this.pos++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else
|
|
422
|
+
this.pos++;
|
|
423
|
+
}
|
|
424
|
+
throw new Structural("an unterminated string");
|
|
425
|
+
}
|
|
426
|
+
skipHeredoc() {
|
|
427
|
+
const head = /<<-?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\n/y;
|
|
428
|
+
head.lastIndex = this.pos;
|
|
429
|
+
const m = head.exec(this.src);
|
|
430
|
+
if (!m)
|
|
431
|
+
throw new Structural("a malformed heredoc");
|
|
432
|
+
const close = new RegExp(`^[ \\t]*${m[1]}[ \\t]*$`, "m");
|
|
433
|
+
const rest = this.src.slice(this.pos + m[0].length);
|
|
434
|
+
const end = close.exec(rest);
|
|
435
|
+
if (!end)
|
|
436
|
+
throw new Structural("an unterminated heredoc");
|
|
437
|
+
this.pos += m[0].length + end.index + end[0].length;
|
|
438
|
+
}
|
|
439
|
+
reference(word) {
|
|
440
|
+
throw new NotLiteral(`a reference (${word}${this.peek() === "." ? "..." : ""})`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* choudoufu's live resource markers, as chant reads them (#2104).
|
|
3
|
+
*
|
|
4
|
+
* On a live root the ownership record is not a state file and not a chant
|
|
5
|
+
* stamp: it is two AWS tags on the resource itself, `tofu-estate` and
|
|
6
|
+
* `tofu-address`, specified in `live/MARKERS.md` in
|
|
7
|
+
* https://github.com/INTENTIUS/choudoufu. That file calls itself the one
|
|
8
|
+
* integration surface external tools may rely on, so this module implements
|
|
9
|
+
* exactly what it specifies and nothing beyond it. The spec's own tables are
|
|
10
|
+
* this module's tests (`./marker.test.ts`).
|
|
11
|
+
*
|
|
12
|
+
* ## Escape, then compare. Never decode a tag blind
|
|
13
|
+
*
|
|
14
|
+
* A canonical terraform address uses `[`, `]` and `"` to write an instance
|
|
15
|
+
* key, and AWS allows none of the three in a tag value. So an address is
|
|
16
|
+
* escaped before it is written, and the spec's comparison rule is one
|
|
17
|
+
* sentence: escape the address you already know and compare strings. The
|
|
18
|
+
* reverse, decoding a tag and hoping to recover the address, is ambiguous
|
|
19
|
+
* (`this[2]` and `this["2"]` escape alike) and the spec says not to do it
|
|
20
|
+
* where a verdict depends on the answer.
|
|
21
|
+
*
|
|
22
|
+
* The rule itself, from "Escaping rule":
|
|
23
|
+
*
|
|
24
|
+
* 1. escape the content of every instance key first (see
|
|
25
|
+
* {@link escapeForEachKey}); a `count` index is only ever digits, so
|
|
26
|
+
* this is a no-op for one;
|
|
27
|
+
* 2. replace every `[` with `:`;
|
|
28
|
+
* 3. delete every `]`;
|
|
29
|
+
* 4. delete every `"`.
|
|
30
|
+
*
|
|
31
|
+
* Step 1 first is what makes a `:` in an escaped value always mean "an index
|
|
32
|
+
* starts here": a key's own `.` and `:` are already gone by the time steps
|
|
33
|
+
* 2 to 4 run.
|
|
34
|
+
*
|
|
35
|
+
* ## Three escapings on the declared side
|
|
36
|
+
*
|
|
37
|
+
* The key escaping gained a layer twice, and neither change rewrote the
|
|
38
|
+
* markers already on live resources. A marker stamped before choudoufu's
|
|
39
|
+
* issue #178 escaped no key at all; one stamped between #178 and #210
|
|
40
|
+
* doubled `@`, `.` and `:` but did not carry out-of-charset characters into
|
|
41
|
+
* the AWS-legal set. So {@link addressMatches} escapes the declared address
|
|
42
|
+
* three ways and accepts any of them, which is what `AddressMatches` does in
|
|
43
|
+
* choudoufu itself. Writing always uses {@link escapeAddress}, the current
|
|
44
|
+
* form.
|
|
45
|
+
*/
|
|
46
|
+
/** `tofu-estate`: the estate that owns the resource. The entire ownership claim. */
|
|
47
|
+
export const MARKER_TAG_ESTATE = "tofu-estate";
|
|
48
|
+
/** `tofu-address`: the resource's escaped canonical address, or its first 256-character chunk. */
|
|
49
|
+
export const MARKER_TAG_ADDRESS = "tofu-address";
|
|
50
|
+
/** `tofu-slot`: the stable, opaque cardinality slot a `count` instance carries. */
|
|
51
|
+
export const MARKER_TAG_SLOT = "tofu-slot";
|
|
52
|
+
/**
|
|
53
|
+
* `tofu-address-2` through `tofu-address-4`, in order. A reader concatenates
|
|
54
|
+
* `tofu-address` and whichever of these are present; they are never
|
|
55
|
+
* meaningful on their own.
|
|
56
|
+
*/
|
|
57
|
+
export const MARKER_TAG_ADDRESS_CONTINUATIONS = [
|
|
58
|
+
"tofu-address-2",
|
|
59
|
+
"tofu-address-3",
|
|
60
|
+
"tofu-address-4",
|
|
61
|
+
];
|
|
62
|
+
/** The characters AWS allows in a tag value beside letters, digits and space. */
|
|
63
|
+
const AWS_LEGAL_PUNCTUATION = new Set(["+", "-", "=", ".", "_", ":", "/", "@"]);
|
|
64
|
+
/** The escape introducer of the out-of-charset layer (choudoufu issue #210). */
|
|
65
|
+
const INTRODUCER = "+";
|
|
66
|
+
function isAwsLegal(ch) {
|
|
67
|
+
return /^[A-Za-z0-9 ]$/.test(ch) || AWS_LEGAL_PUNCTUATION.has(ch);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Carry every character outside the AWS-legal set into it: `+` doubles
|
|
71
|
+
* because it is the introducer, and anything else becomes `+` followed by its
|
|
72
|
+
* Unicode code point in six uppercase hex digits. `a(b)` becomes
|
|
73
|
+
* `a+000028b+000029`; `plus+one` becomes `plus++one`.
|
|
74
|
+
*/
|
|
75
|
+
function escapeOutOfCharset(key) {
|
|
76
|
+
let out = "";
|
|
77
|
+
for (const ch of key) {
|
|
78
|
+
if (ch === INTRODUCER)
|
|
79
|
+
out += "++";
|
|
80
|
+
else if (isAwsLegal(ch))
|
|
81
|
+
out += ch;
|
|
82
|
+
else
|
|
83
|
+
out += INTRODUCER + ch.codePointAt(0).toString(16).toUpperCase().padStart(6, "0");
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The `@` / `.` / `:` doubling (choudoufu issue #178), applied in that order
|
|
89
|
+
* so every `@` these steps introduce is never itself doubled again: `@`
|
|
90
|
+
* becomes `@@`, then `.` becomes `@d`, then `:` becomes `@c`.
|
|
91
|
+
*/
|
|
92
|
+
function escapeReservedPunctuation(key) {
|
|
93
|
+
let out = "";
|
|
94
|
+
for (const ch of key) {
|
|
95
|
+
if (ch === "@")
|
|
96
|
+
out += "@@";
|
|
97
|
+
else if (ch === ".")
|
|
98
|
+
out += "@d";
|
|
99
|
+
else if (ch === ":")
|
|
100
|
+
out += "@c";
|
|
101
|
+
else
|
|
102
|
+
out += ch;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Escape one `for_each` instance key, both layers in the specified order:
|
|
108
|
+
* out-of-charset escaping first, then the `@` / `.` / `:` doubling over its
|
|
109
|
+
* output. Order is load-bearing: the first layer's introducer is `+`, none of
|
|
110
|
+
* the three characters the second layer scans for, so the second never
|
|
111
|
+
* mistakes the first's output for its own escape sequences.
|
|
112
|
+
*
|
|
113
|
+
* Six characters cannot be carried at all and are refused at choudoufu's own
|
|
114
|
+
* lint rather than here (`"`, `\`, any non-printable, `$`, `%`, `[`, `]`);
|
|
115
|
+
* this function escapes what it is given without judging it, because chant
|
|
116
|
+
* never writes a marker, and a key chant meets in a `tofu-address` tag has
|
|
117
|
+
* already passed that lint.
|
|
118
|
+
*/
|
|
119
|
+
export function escapeForEachKey(key) {
|
|
120
|
+
return escapeReservedPunctuation(escapeOutOfCharset(key));
|
|
121
|
+
}
|
|
122
|
+
/** The pre-#210 key escaping: the doubling alone, with no out-of-charset layer. */
|
|
123
|
+
function escapeForEachKeyPre210(key) {
|
|
124
|
+
return escapeReservedPunctuation(key);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Apply `escapeKey` to the content of every quoted instance key in `address`,
|
|
128
|
+
* then run the address-level substitution: `[` to `:`, `]` and `"` deleted.
|
|
129
|
+
*/
|
|
130
|
+
function escapeAddressWith(address, escapeKey) {
|
|
131
|
+
// Only a quoted key carries content that needs escaping; a `count` index is
|
|
132
|
+
// digits, which every layer leaves alone.
|
|
133
|
+
const keysEscaped = address.replace(/\["((?:[^"\\]|\\.)*)"\]/g, (_all, key) => `["${escapeKey(key)}"]`);
|
|
134
|
+
return keysEscaped.replace(/\[/g, ":").replace(/\]/g, "").replace(/"/g, "");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The `tofu-address` value for a canonical terraform address: the current
|
|
138
|
+
* escaping, which is the only one anything ever writes.
|
|
139
|
+
*/
|
|
140
|
+
export function escapeAddress(address) {
|
|
141
|
+
return escapeAddressWith(address, escapeForEachKey);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Every escaping a live marker could have been written under, current first:
|
|
145
|
+
* the current one, the pre-#210 one (the doubling alone), and the pre-#178
|
|
146
|
+
* one (no key escaping at all). Duplicates are collapsed, so the common
|
|
147
|
+
* address with no `for_each` key yields exactly one string.
|
|
148
|
+
*/
|
|
149
|
+
export function escapeAddressVariants(address) {
|
|
150
|
+
const variants = [
|
|
151
|
+
escapeAddress(address),
|
|
152
|
+
escapeAddressWith(address, escapeForEachKeyPre210),
|
|
153
|
+
escapeAddressWith(address, (key) => key),
|
|
154
|
+
];
|
|
155
|
+
return [...new Set(variants)];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Does a live resource's `tofu-address` value name this declared address?
|
|
159
|
+
* The declared side is escaped and the strings are compared, three escapings
|
|
160
|
+
* deep for the migration windows; the tag is never decoded.
|
|
161
|
+
*/
|
|
162
|
+
export function addressMatches(declaredAddress, tagValue) {
|
|
163
|
+
return escapeAddressVariants(declaredAddress).includes(tagValue);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Concatenate `tofu-address` and whatever continuation tags follow it, which
|
|
167
|
+
* is the only sanctioned way to read a split address.
|
|
168
|
+
*
|
|
169
|
+
* A chain with a gap in it (`tofu-address-3` present while `tofu-address-2`
|
|
170
|
+
* is not) cannot be concatenated into anything and returns `undefined` rather
|
|
171
|
+
* than the address up to the gap.
|
|
172
|
+
*/
|
|
173
|
+
export function joinMarkerAddress(tags) {
|
|
174
|
+
const head = tags[MARKER_TAG_ADDRESS];
|
|
175
|
+
if (head === undefined || head === "")
|
|
176
|
+
return undefined;
|
|
177
|
+
let address = head;
|
|
178
|
+
let ended = false;
|
|
179
|
+
for (const key of MARKER_TAG_ADDRESS_CONTINUATIONS) {
|
|
180
|
+
const chunk = tags[key];
|
|
181
|
+
if (chunk === undefined || chunk === "") {
|
|
182
|
+
ended = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ended)
|
|
186
|
+
return undefined; // a gap: the chain is malformed, not shorter
|
|
187
|
+
address += chunk;
|
|
188
|
+
}
|
|
189
|
+
return address;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The ownership verdict `live/MARKERS.md`'s "Ownership semantics" section
|
|
193
|
+
* defines, read off one live resource's tags. Three outcomes, and no fourth:
|
|
194
|
+
* a `tofu-estate` value is the entire ownership claim with no secondary
|
|
195
|
+
* check, neither key present is foreign, and an estate without a readable
|
|
196
|
+
* address is malformed rather than either of the two.
|
|
197
|
+
*/
|
|
198
|
+
export function readMarker(tags) {
|
|
199
|
+
const estate = tags?.[MARKER_TAG_ESTATE];
|
|
200
|
+
if (estate === undefined || estate === "")
|
|
201
|
+
return { kind: "foreign" };
|
|
202
|
+
const address = joinMarkerAddress(tags);
|
|
203
|
+
if (address === undefined) {
|
|
204
|
+
return {
|
|
205
|
+
kind: "malformed",
|
|
206
|
+
estate,
|
|
207
|
+
detail: `carries ${MARKER_TAG_ESTATE}=${estate} but no readable ${MARKER_TAG_ADDRESS} (missing, empty, or a continuation chain with a gap in it)`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const slot = tags[MARKER_TAG_SLOT];
|
|
211
|
+
return { kind: "owned", estate, address, ...(slot ? { slot } : {}) };
|
|
212
|
+
}
|