codeterrain-terraform 0.2.0__py3-none-any.whl
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.
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Terraform for codeterrain: the definitions and references of ``.tf`` files.
|
|
2
|
+
|
|
3
|
+
Definitions are named the way Terraform code refers to them:
|
|
4
|
+
|
|
5
|
+
- ``resource "aws_s3_bucket" "logs"`` is ``aws_s3_bucket.logs``;
|
|
6
|
+
- ``data "aws_ami" "base"`` is ``data.aws_ami.base``;
|
|
7
|
+
- ``module "vpc"`` is ``module.vpc``, ``variable "region"`` is ``var.region``, an entry of a
|
|
8
|
+
``locals`` block is ``local.name`` and ``output "url"`` is ``output.url``;
|
|
9
|
+
- an entry of ``required_providers`` is ``provider.aws``, and ``provider "aws"`` is
|
|
10
|
+
``provider.aws`` too, or ``provider.aws.east`` with ``alias = "east"``.
|
|
11
|
+
|
|
12
|
+
A module call's doc is its ``source`` and ``version``, a required provider's the same; a
|
|
13
|
+
resource's ``provider = aws.east`` and a module's ``providers = { aws = aws.east }`` are
|
|
14
|
+
references to ``provider.aws.east``.
|
|
15
|
+
|
|
16
|
+
References are the expressions that name one of those (``var.region``, ``aws_s3_bucket.logs.arn``,
|
|
17
|
+
``module.vpc.id``), in attribute values and inside string and heredoc templates, with the
|
|
18
|
+
definition they sit in as their scope. The reader is a tokenizer, not an evaluator: it follows no
|
|
19
|
+
module source, expands no ``count`` or ``for_each`` and reads no ``.tfvars``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import string
|
|
26
|
+
import sys
|
|
27
|
+
import textwrap
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
__version__ = version("codeterrain-terraform")
|
|
34
|
+
except PackageNotFoundError: # run from a source tree
|
|
35
|
+
__version__ = "0"
|
|
36
|
+
|
|
37
|
+
#: Labels each top-level block takes, and how its definition is named.
|
|
38
|
+
_BLOCKS = {"resource": 2, "data": 2, "module": 1, "variable": 1, "output": 1, "provider": 1}
|
|
39
|
+
_PREFIX = {"resource": "", "data": "data.", "module": "module.", "variable": "var.",
|
|
40
|
+
"output": "output.", "provider": "provider."}
|
|
41
|
+
#: Objects whose attributes are never a definition of this configuration.
|
|
42
|
+
_BUILTIN = frozenset({"each", "count", "path", "terraform", "self"})
|
|
43
|
+
#: A resource type: a provider name, ``_``, then the type (``aws_s3_bucket``, ``random_id``).
|
|
44
|
+
_RESOURCE_TYPE = re.compile(r"[a-z][a-z0-9]*_[a-z0-9_]+\Z")
|
|
45
|
+
#: A Terraform identifier: Unicode letters, digits, ``_`` and ``-``, not starting with a digit.
|
|
46
|
+
_IDENT = re.compile(r"[^\W\d][\w-]*")
|
|
47
|
+
#: Operators of more than one character, kept as one token so ``==`` is never read as ``=``.
|
|
48
|
+
_OPERATOR = re.compile(r"==|!=|<=|>=|=>|&&|\|\||\.\.\.|::")
|
|
49
|
+
_HEREDOC = re.compile(r"<<-?([^\W\d][\w-]*)[ \t]*\r?\n")
|
|
50
|
+
#: What a quoted string's single-character escapes stand for.
|
|
51
|
+
_ESCAPES = {"n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"}
|
|
52
|
+
#: Deepest nesting of templates inside templates read before the file is given up.
|
|
53
|
+
MAX_NESTING = 64
|
|
54
|
+
MAX_DOC = 200
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TerraformError(ValueError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Tok:
|
|
63
|
+
#: "ident", "str" (a literal string: its text), "punct", "mark" (a directive's ``%{`` or end)
|
|
64
|
+
kind: str
|
|
65
|
+
value: str
|
|
66
|
+
line: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _Lexer:
|
|
70
|
+
def __init__(self, src: str) -> None:
|
|
71
|
+
self.src = src
|
|
72
|
+
self.i = 0
|
|
73
|
+
self.line = 1
|
|
74
|
+
self.out: list[Tok] = []
|
|
75
|
+
|
|
76
|
+
def _advance(self, to: int) -> None:
|
|
77
|
+
self.line += self.src.count("\n", self.i, to)
|
|
78
|
+
self.i = to
|
|
79
|
+
|
|
80
|
+
def run(self) -> list[Tok]:
|
|
81
|
+
self._code(stop=None, depth=0)
|
|
82
|
+
return self.out
|
|
83
|
+
|
|
84
|
+
def _code(self, stop: str | None, depth: int) -> None:
|
|
85
|
+
"""Tokens up to the ``}`` that closes an interpolation (``stop``), or the end."""
|
|
86
|
+
if depth > MAX_NESTING:
|
|
87
|
+
raise TerraformError("templates nested too deeply")
|
|
88
|
+
src, braces = self.src, 0
|
|
89
|
+
while self.i < len(src):
|
|
90
|
+
c = src[self.i]
|
|
91
|
+
if c == "\n" or c in " \t\r":
|
|
92
|
+
self._advance(self.i + 1)
|
|
93
|
+
elif c == "#" or src.startswith("//", self.i):
|
|
94
|
+
end = src.find("\n", self.i)
|
|
95
|
+
self._advance(len(src) if end < 0 else end)
|
|
96
|
+
elif src.startswith("/*", self.i):
|
|
97
|
+
end = src.find("*/", self.i + 2)
|
|
98
|
+
self._advance(len(src) if end < 0 else end + 2)
|
|
99
|
+
elif c == '"':
|
|
100
|
+
self._string(depth)
|
|
101
|
+
elif src.startswith("<<", self.i) and (m := _HEREDOC.match(src, self.i)):
|
|
102
|
+
self._heredoc(m, depth)
|
|
103
|
+
elif m := _IDENT.match(src, self.i):
|
|
104
|
+
self.out.append(Tok("ident", m.group(), self.line))
|
|
105
|
+
self._advance(m.end())
|
|
106
|
+
elif m := _OPERATOR.match(src, self.i):
|
|
107
|
+
self.out.append(Tok("punct", m.group(), self.line))
|
|
108
|
+
self._advance(m.end())
|
|
109
|
+
elif c in "{[(":
|
|
110
|
+
braces += c == "{"
|
|
111
|
+
self.out.append(Tok("punct", c, self.line))
|
|
112
|
+
self._advance(self.i + 1)
|
|
113
|
+
elif c == "}" and stop is not None and braces == 0:
|
|
114
|
+
self._advance(self.i + 1)
|
|
115
|
+
return
|
|
116
|
+
else:
|
|
117
|
+
braces -= c == "}"
|
|
118
|
+
self.out.append(Tok("punct", c, self.line))
|
|
119
|
+
self._advance(self.i + 1)
|
|
120
|
+
|
|
121
|
+
def _template(self, end: int, depth: int, quoted: bool = True) -> str:
|
|
122
|
+
"""Read template text up to ``end`` (exclusive), or in a quoted string up to its closing
|
|
123
|
+
quote or the end of its line, lexing each ``${...}`` as code: the code lexer finds where
|
|
124
|
+
an interpolation ends, so its comments, strings and braces never end the template. The
|
|
125
|
+
literal text, for a block label. A backslash escapes only in a quoted string: a heredoc
|
|
126
|
+
keeps it as text."""
|
|
127
|
+
src, text = self.src, []
|
|
128
|
+
while self.i < end:
|
|
129
|
+
if quoted and src[self.i] in '"\n':
|
|
130
|
+
break
|
|
131
|
+
if src.startswith(("$${", "%%{"), self.i):
|
|
132
|
+
text.append(src[self.i + 1:self.i + 3])
|
|
133
|
+
self._advance(self.i + 3)
|
|
134
|
+
elif src.startswith(("${", "%{"), self.i):
|
|
135
|
+
directive = src[self.i] == "%"
|
|
136
|
+
if directive:
|
|
137
|
+
self.out.append(Tok("mark", "%{", self.line))
|
|
138
|
+
# a ``~`` strip marker (``%{~ endfor }``) is part of the opener, not code
|
|
139
|
+
self._advance(self.i + 2 + src.startswith("~", self.i + 2))
|
|
140
|
+
self._code(stop="}", depth=depth + 1)
|
|
141
|
+
if directive:
|
|
142
|
+
self.out.append(Tok("mark", "%}", self.line))
|
|
143
|
+
elif quoted and src[self.i] == "\\" and self.i + 1 < end:
|
|
144
|
+
text.append(self._escape())
|
|
145
|
+
else:
|
|
146
|
+
text.append(src[self.i])
|
|
147
|
+
self._advance(self.i + 1)
|
|
148
|
+
return "".join(text)
|
|
149
|
+
|
|
150
|
+
def _escape(self) -> str:
|
|
151
|
+
"""The character a quoted string's escape at ``self.i`` stands for (``\\n``, ``\\t``,
|
|
152
|
+
``\\u00e9``), moving past it; an unknown escape keeps the character after the backslash."""
|
|
153
|
+
src, i = self.src, self.i
|
|
154
|
+
kind = src[i + 1]
|
|
155
|
+
width = {"u": 4, "U": 8}.get(kind)
|
|
156
|
+
if width:
|
|
157
|
+
digits = src[i + 2:i + 2 + width]
|
|
158
|
+
if len(digits) == width and all(c in string.hexdigits for c in digits):
|
|
159
|
+
code = int(digits, 16)
|
|
160
|
+
if code <= sys.maxunicode and not 0xD800 <= code <= 0xDFFF: # no lone surrogate
|
|
161
|
+
self._advance(i + 2 + width)
|
|
162
|
+
return chr(code)
|
|
163
|
+
self._advance(i + 2)
|
|
164
|
+
return _ESCAPES.get(kind, kind)
|
|
165
|
+
|
|
166
|
+
def _string(self, depth: int) -> None:
|
|
167
|
+
line = self.line
|
|
168
|
+
self._advance(self.i + 1)
|
|
169
|
+
text = self._template(len(self.src), depth)
|
|
170
|
+
self.out.append(Tok("str", text, line))
|
|
171
|
+
if self.i < len(self.src) and self.src[self.i] == '"':
|
|
172
|
+
self._advance(self.i + 1)
|
|
173
|
+
|
|
174
|
+
def _heredoc(self, m: re.Match[str], depth: int) -> None:
|
|
175
|
+
line = self.line
|
|
176
|
+
self._advance(m.end())
|
|
177
|
+
close = re.compile(rf"^[ \t]*{re.escape(m.group(1))}[ \t]*\r?$", re.M)
|
|
178
|
+
found = close.search(self.src, self.i)
|
|
179
|
+
end = found.start() if found else len(self.src)
|
|
180
|
+
text = self._template(end, depth, quoted=False)
|
|
181
|
+
# the literal text, for a description: ``<<-`` strips the common indentation
|
|
182
|
+
self.out.append(Tok("str", textwrap.dedent(text).strip(), line))
|
|
183
|
+
if found:
|
|
184
|
+
self._advance(found.end())
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _punct(toks: list[Tok], i: int, value: str) -> bool:
|
|
188
|
+
"""Whether ``toks[i]`` is the punctuation ``value``: never a string's text that reads the same
|
|
189
|
+
(the ``.`` that ends ``"${var.domain}."``)."""
|
|
190
|
+
return 0 <= i < len(toks) and toks[i].kind == "punct" and toks[i].value == value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _entry(toks: list[Tok], i: int) -> bool:
|
|
194
|
+
"""Whether ``toks[i]`` separates an object entry's key from its value: ``=`` or ``:``. A
|
|
195
|
+
block body's attributes take only ``=``."""
|
|
196
|
+
return _punct(toks, i, "=") or _punct(toks, i, ":")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _mark(toks: list[Tok], i: int, value: str) -> bool:
|
|
200
|
+
"""Whether ``toks[i]`` is where a template directive starts (``%{``) or ends (``%}``)."""
|
|
201
|
+
return 0 <= i < len(toks) and toks[i].kind == "mark" and toks[i].value == value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _traversal(toks: list[Tok], i: int) -> list[str]:
|
|
205
|
+
"""The names of ``a.b.c`` starting at ``toks[i]``, up to an index or the end."""
|
|
206
|
+
parts = [toks[i].value]
|
|
207
|
+
j = i + 1
|
|
208
|
+
while j + 1 < len(toks) and _punct(toks, j, ".") and toks[j + 1].kind == "ident":
|
|
209
|
+
parts.append(toks[j + 1].value)
|
|
210
|
+
j += 2
|
|
211
|
+
return parts
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _iterators(toks: list[Tok]) -> frozenset[str]:
|
|
215
|
+
"""The iterator names of the file's ``dynamic`` blocks: the label (``dynamic "rule"`` gives
|
|
216
|
+
``rule.value``), or the ``iterator = name`` that replaces it, written directly in that
|
|
217
|
+
block's body."""
|
|
218
|
+
found = set()
|
|
219
|
+
depth = 0
|
|
220
|
+
bodies: list[int] = [] # the brace depth of each open dynamic block's body
|
|
221
|
+
for i, tok in enumerate(toks):
|
|
222
|
+
if tok.kind == "punct" and tok.value == "{":
|
|
223
|
+
depth += 1
|
|
224
|
+
elif tok.kind == "punct" and tok.value == "}":
|
|
225
|
+
depth -= 1
|
|
226
|
+
while bodies and bodies[-1] > depth:
|
|
227
|
+
bodies.pop()
|
|
228
|
+
elif i + 2 < len(toks) and tok.kind == "ident":
|
|
229
|
+
if tok.value == "dynamic" and toks[i + 1].kind == "str" and _punct(toks, i + 2, "{"):
|
|
230
|
+
found.add(toks[i + 1].value)
|
|
231
|
+
bodies.append(depth + 1)
|
|
232
|
+
elif (
|
|
233
|
+
tok.value == "iterator" and bodies and bodies[-1] == depth
|
|
234
|
+
and _punct(toks, i + 1, "=") and toks[i + 2].kind == "ident"
|
|
235
|
+
and not _punct(toks, i + 3, ".")
|
|
236
|
+
):
|
|
237
|
+
found.add(toks[i + 2].value)
|
|
238
|
+
return frozenset(found)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _bound(toks: list[Tok]) -> set[int]:
|
|
242
|
+
"""The indexes of the tokens that name a ``for`` binding rather than the configuration:
|
|
243
|
+
``local`` in ``[for local in var.items : local.name]`` reads the element. An expression's
|
|
244
|
+
names hold from its ``:`` to its closing bracket; a template directive's from the end of
|
|
245
|
+
``%{ for x in xs }`` to its ``%{ endfor }``."""
|
|
246
|
+
found: set[int] = set()
|
|
247
|
+
frames: list[set[str]] = [] # the names bound inside each open bracket
|
|
248
|
+
questions: list[int] = [] # the ``?`` of each open bracket still waiting for their ``:``
|
|
249
|
+
directives: list[set[str]] = []
|
|
250
|
+
# Each ``for`` whose collection is still being read: its names, its bracket depth (None: a
|
|
251
|
+
# directive) and the directive it opens (how many were open with it). A ``for`` nested in
|
|
252
|
+
# that collection stacks above it.
|
|
253
|
+
pending: list[tuple[set[str], int | None, int]] = []
|
|
254
|
+
open_marks = 0 # directives opened and not yet ended
|
|
255
|
+
for i, tok in enumerate(toks):
|
|
256
|
+
if tok.kind == "punct" and tok.value in "{[(":
|
|
257
|
+
frames.append(set())
|
|
258
|
+
questions.append(0)
|
|
259
|
+
elif tok.kind == "punct" and tok.value in "}])":
|
|
260
|
+
if frames:
|
|
261
|
+
closed = len(frames)
|
|
262
|
+
frames.pop()
|
|
263
|
+
questions.pop()
|
|
264
|
+
pending = [p for p in pending if p[1] is None or p[1] < closed]
|
|
265
|
+
elif tok.kind == "punct" and tok.value == "?" and questions:
|
|
266
|
+
questions[-1] += 1
|
|
267
|
+
elif tok.kind == "punct" and tok.value == ":" and questions and questions[-1]:
|
|
268
|
+
questions[-1] -= 1 # the ``:`` of a conditional
|
|
269
|
+
elif tok.kind == "punct" and tok.value == ":" and pending and pending[-1][1] == len(frames):
|
|
270
|
+
if frames:
|
|
271
|
+
frames[-1] |= pending[-1][0]
|
|
272
|
+
pending.pop()
|
|
273
|
+
elif _mark(toks, i, "%{"):
|
|
274
|
+
open_marks += 1
|
|
275
|
+
elif _mark(toks, i, "%}"):
|
|
276
|
+
# only the end of the ``for`` directive's own ``%{ ... }`` starts its binding
|
|
277
|
+
if pending and pending[-1][1] is None and pending[-1][2] == open_marks:
|
|
278
|
+
directives.append(pending.pop()[0])
|
|
279
|
+
open_marks = max(open_marks - 1, 0)
|
|
280
|
+
elif tok.kind == "ident" and tok.value == "endfor" and _mark(toks, i - 1, "%{"):
|
|
281
|
+
if directives:
|
|
282
|
+
directives.pop()
|
|
283
|
+
elif tok.kind == "ident" and tok.value == "for":
|
|
284
|
+
names, j = set(), i + 1
|
|
285
|
+
while j < len(toks) and toks[j].kind == "ident" and toks[j].value != "in":
|
|
286
|
+
names.add(toks[j].value)
|
|
287
|
+
j += 1 + _punct(toks, j + 1, ",")
|
|
288
|
+
if j < len(toks) and toks[j].kind == "ident" and toks[j].value == "in" and names:
|
|
289
|
+
if _punct(toks, i - 1, "{") or _punct(toks, i - 1, "["):
|
|
290
|
+
pending.append((names, len(frames), 0))
|
|
291
|
+
elif _mark(toks, i - 1, "%{"):
|
|
292
|
+
pending.append((names, None, open_marks))
|
|
293
|
+
elif (
|
|
294
|
+
tok.kind == "ident" and not _punct(toks, i - 1, ".")
|
|
295
|
+
and any(tok.value in names for names in (*frames, *directives))
|
|
296
|
+
):
|
|
297
|
+
found.add(i)
|
|
298
|
+
return found
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _reference(parts: list[str], iterators: frozenset[str] = frozenset()) -> str | None:
|
|
302
|
+
head = parts[0]
|
|
303
|
+
if head in iterators and parts[1] in ("key", "value"):
|
|
304
|
+
return None
|
|
305
|
+
if head in ("var", "local", "module") and len(parts) >= 2:
|
|
306
|
+
return f"{head}.{parts[1]}"
|
|
307
|
+
if head == "data" and len(parts) >= 3:
|
|
308
|
+
return f"data.{parts[1]}.{parts[2]}"
|
|
309
|
+
if head not in _BUILTIN and len(parts) >= 2 and _RESOURCE_TYPE.match(head):
|
|
310
|
+
return f"{head}.{parts[1]}"
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def read(source: str) -> dict[str, Any]:
|
|
315
|
+
toks = _Lexer(source).run()
|
|
316
|
+
iterators = _iterators(toks)
|
|
317
|
+
bound = _bound(toks)
|
|
318
|
+
defs: list[dict[str, Any]] = []
|
|
319
|
+
refs: list[dict[str, Any]] = []
|
|
320
|
+
depth, nest, scope, kind = 0, 0, "", "" # nest: open ( and [ inside the block body
|
|
321
|
+
current: dict[str, Any] | None = None
|
|
322
|
+
#: ``source`` and ``version`` of a module or required provider, for its doc
|
|
323
|
+
settings: dict[int, dict[str, str]] = {}
|
|
324
|
+
first_ref = 0 # the first reference of the current block, renamed with a provider alias
|
|
325
|
+
required = 0 # the depth of the body of ``required_providers``, while inside it
|
|
326
|
+
wanted_provider: dict[str, Any] | None = None # the required provider being read
|
|
327
|
+
providers = 0 # the depth of the body of a module's ``providers`` map, while inside it
|
|
328
|
+
i = 0
|
|
329
|
+
while i < len(toks):
|
|
330
|
+
tok = toks[i]
|
|
331
|
+
if tok.kind == "punct":
|
|
332
|
+
if tok.value == "{":
|
|
333
|
+
depth += 1
|
|
334
|
+
elif tok.value == "}":
|
|
335
|
+
depth = max(depth - 1, 0)
|
|
336
|
+
if depth < required:
|
|
337
|
+
required = 0
|
|
338
|
+
if depth <= required:
|
|
339
|
+
wanted_provider = None
|
|
340
|
+
if depth < providers:
|
|
341
|
+
providers = 0
|
|
342
|
+
if depth == 0:
|
|
343
|
+
scope, kind, current, nest = "", "", None, 0
|
|
344
|
+
elif tok.value in "([":
|
|
345
|
+
nest += 1
|
|
346
|
+
elif tok.value in ")]":
|
|
347
|
+
nest = max(nest - 1, 0)
|
|
348
|
+
i += 1
|
|
349
|
+
continue
|
|
350
|
+
if depth == 0 and tok.kind == "ident":
|
|
351
|
+
labels = []
|
|
352
|
+
j = i + 1
|
|
353
|
+
while j < len(toks) and toks[j].kind in ("str", "ident"):
|
|
354
|
+
labels.append(toks[j].value)
|
|
355
|
+
j += 1
|
|
356
|
+
if _punct(toks, j, "{"):
|
|
357
|
+
kind = tok.value
|
|
358
|
+
wanted = _BLOCKS.get(kind)
|
|
359
|
+
if wanted is not None and len(labels) == wanted:
|
|
360
|
+
scope = _PREFIX[kind] + ".".join(labels)
|
|
361
|
+
current = {"name": scope, "kind": kind, "line": tok.line, "doc": ""}
|
|
362
|
+
defs.append(current)
|
|
363
|
+
else:
|
|
364
|
+
scope, current = "", None
|
|
365
|
+
first_ref = len(refs)
|
|
366
|
+
depth = 1
|
|
367
|
+
i = j + 1
|
|
368
|
+
continue
|
|
369
|
+
if (
|
|
370
|
+
kind == "terraform" and depth == 1 and nest == 0 and tok.kind == "ident"
|
|
371
|
+
and tok.value == "required_providers" and _punct(toks, i + 1, "{")
|
|
372
|
+
):
|
|
373
|
+
required = 2
|
|
374
|
+
i += 1
|
|
375
|
+
continue
|
|
376
|
+
# A required provider: ``aws = { source = "hashicorp/aws", version = "~> 5.0" }``, or
|
|
377
|
+
# the older ``aws = "~> 5.0"``.
|
|
378
|
+
if (
|
|
379
|
+
required and depth == required and nest == 0 and tok.kind == "ident"
|
|
380
|
+
and _punct(toks, i + 1, "=")
|
|
381
|
+
):
|
|
382
|
+
wanted_provider = {
|
|
383
|
+
"name": f"provider.{tok.value}", "kind": "required_provider", "line": tok.line,
|
|
384
|
+
"doc": "",
|
|
385
|
+
}
|
|
386
|
+
defs.append(wanted_provider)
|
|
387
|
+
if i + 2 < len(toks) and toks[i + 2].kind == "str":
|
|
388
|
+
settings[id(wanted_provider)] = {"version": toks[i + 2].value}
|
|
389
|
+
i += 3
|
|
390
|
+
else:
|
|
391
|
+
i += 2
|
|
392
|
+
continue
|
|
393
|
+
if (
|
|
394
|
+
wanted_provider is not None and depth == required + 1 and nest == 0
|
|
395
|
+
and tok.kind == "ident" and tok.value in ("source", "version")
|
|
396
|
+
and _entry(toks, i + 1) and i + 2 < len(toks) and toks[i + 2].kind == "str"
|
|
397
|
+
):
|
|
398
|
+
settings.setdefault(id(wanted_provider), {})[tok.value] = toks[i + 2].value
|
|
399
|
+
i += 3
|
|
400
|
+
continue
|
|
401
|
+
# The provider configurations of a module call: ``providers = { aws = aws.east }``.
|
|
402
|
+
if (
|
|
403
|
+
providers and depth == providers and tok.kind == "ident"
|
|
404
|
+
and _entry(toks, i - 1)
|
|
405
|
+
):
|
|
406
|
+
parts = _traversal(toks, i)
|
|
407
|
+
refs.append({"name": "provider." + ".".join(parts[:2]), "line": tok.line,
|
|
408
|
+
"scope": scope})
|
|
409
|
+
i += 2 * len(parts) - 1
|
|
410
|
+
continue
|
|
411
|
+
# An attribute: ``name =`` starting a line of the block body, outside brackets.
|
|
412
|
+
if (
|
|
413
|
+
tok.kind == "ident" and depth == 1 and nest == 0
|
|
414
|
+
and _punct(toks, i + 1, "=")
|
|
415
|
+
and (i == 0 or toks[i - 1].line < tok.line or _punct(toks, i - 1, "{"))
|
|
416
|
+
):
|
|
417
|
+
if kind == "locals":
|
|
418
|
+
scope = f"local.{tok.value}"
|
|
419
|
+
defs.append({"name": scope, "kind": "local", "line": tok.line, "doc": ""})
|
|
420
|
+
i += 2
|
|
421
|
+
continue
|
|
422
|
+
if (
|
|
423
|
+
current is not None and tok.value == "description" and i + 2 < len(toks)
|
|
424
|
+
and toks[i + 2].kind == "str"
|
|
425
|
+
):
|
|
426
|
+
current["doc"] = toks[i + 2].value[:MAX_DOC]
|
|
427
|
+
literal = toks[i + 2].value if i + 2 < len(toks) and toks[i + 2].kind == "str" else None
|
|
428
|
+
if current is not None and kind == "module" and tok.value in ("source", "version"):
|
|
429
|
+
if literal is not None:
|
|
430
|
+
settings.setdefault(id(current), {})[tok.value] = literal
|
|
431
|
+
elif current is not None and kind == "provider" and tok.value == "alias":
|
|
432
|
+
# ``provider "aws" { alias = "east" }`` is what ``provider = aws.east`` names
|
|
433
|
+
if literal is not None and _IDENT.fullmatch(literal):
|
|
434
|
+
scope = current["name"] = f"{current['name']}.{literal}"
|
|
435
|
+
for ref in refs[first_ref:]:
|
|
436
|
+
ref["scope"] = scope
|
|
437
|
+
elif kind in ("resource", "data") and tok.value == "provider" and i + 2 < len(toks):
|
|
438
|
+
if toks[i + 2].kind == "ident":
|
|
439
|
+
parts = _traversal(toks, i + 2)
|
|
440
|
+
refs.append({"name": "provider." + ".".join(parts[:2]), "line": tok.line,
|
|
441
|
+
"scope": scope})
|
|
442
|
+
i += 2 + 2 * len(parts) - 1
|
|
443
|
+
continue
|
|
444
|
+
elif kind == "module" and tok.value == "providers" and _punct(toks, i + 2, "{"):
|
|
445
|
+
providers = 2
|
|
446
|
+
if tok.kind == "ident" and not _punct(toks, i - 1, "."):
|
|
447
|
+
parts = _traversal(toks, i)
|
|
448
|
+
name = _reference(parts, iterators) if len(parts) > 1 and i not in bound else None
|
|
449
|
+
if name is not None:
|
|
450
|
+
refs.append({"name": name, "line": tok.line, "scope": scope})
|
|
451
|
+
i += max(1, 2 * len(parts) - 1)
|
|
452
|
+
continue
|
|
453
|
+
i += 1
|
|
454
|
+
for d in defs:
|
|
455
|
+
found = settings.get(id(d))
|
|
456
|
+
if found and not d["doc"]:
|
|
457
|
+
d["doc"] = ", ".join(
|
|
458
|
+
f"{key} {found[key]}" for key in ("source", "version") if key in found
|
|
459
|
+
)[:MAX_DOC]
|
|
460
|
+
return {"defs": defs, "refs": refs}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class _Language:
|
|
464
|
+
interface = 1
|
|
465
|
+
suffixes = (".tf",)
|
|
466
|
+
read = staticmethod(read)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
#: The entry point codeterrain loads (``codeterrain.languages``: ``terraform``).
|
|
470
|
+
language = _Language()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: codeterrain-terraform
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Terraform for codeterrain: resources, modules, providers and references of .tf files in terrain def and refs.
|
|
5
|
+
Project-URL: Homepage, https://github.com/benchli-sports/codeterrain
|
|
6
|
+
Project-URL: Issues, https://github.com/benchli-sports/codeterrain/issues
|
|
7
|
+
Author: Mitbolas GmbH
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: codeterrain>=0.3
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# codeterrain-terraform
|
|
14
|
+
|
|
15
|
+
Terraform for [codeterrain](https://github.com/benchli-sports/codeterrain): the definitions and
|
|
16
|
+
references of `.tf` files, in `terrain def` and `terrain refs`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
uv tool install "<path-to-the-codeterrain-checkout>[terraform]"
|
|
20
|
+
terrain def logs # resource aws_s3_bucket.logs infra/storage.tf:12
|
|
21
|
+
terrain refs var.region # every expression that reads it, and the block it sits in
|
|
22
|
+
terrain def --kind module # every module call, with its file, source and version
|
|
23
|
+
terrain def --kind required_provider # every pinned provider and its version constraint
|
|
24
|
+
terrain refs provider.aws.east # the resources and module calls that select that provider
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Not published yet: install from a checkout of the codeterrain repository, where this pack lives under
|
|
28
|
+
`packages/`. Once published, `pip install "codeterrain[terraform]"` will do the same.
|
|
29
|
+
`terrain doctor` lists the installed packs (`language packs: terraform 0.2.0`).
|
|
30
|
+
|
|
31
|
+
Names follow Terraform's own references: `aws_s3_bucket.logs`, `data.aws_ami.base`, `module.vpc`,
|
|
32
|
+
`var.region`, `local.tags`, `output.url`, and `provider.aws` for both an entry of
|
|
33
|
+
`required_providers` (kind `required_provider`) and a `provider "aws"` block (kind `provider`;
|
|
34
|
+
`provider.aws.east` with `alias = "east"`). A module call's doc is its `source` and `version`, as
|
|
35
|
+
is a required provider's. A resource's `provider = aws.east` and a module's
|
|
36
|
+
`providers = { aws = aws.east }` are references to `provider.aws.east`. References are found in attribute values and inside string
|
|
37
|
+
and heredoc templates. The reader is a tokenizer, not an evaluator: it follows no module source,
|
|
38
|
+
expands no `count` or `for_each` and reads no `.tfvars`. A resource that selects no provider uses
|
|
39
|
+
its default one implicitly; that is not listed as a reference. `.tf.json` files are not read.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
codeterrain_terraform/__init__.py,sha256=kVHa0A-B7Oy1jPZ382exmv2dUwnneF7vrRGutGYVWeU,21134
|
|
2
|
+
codeterrain_terraform-0.2.0.dist-info/METADATA,sha256=jubjrvWYx3g5QYq9N6wlLW9iWtWYg550jRl00aDq9N8,2320
|
|
3
|
+
codeterrain_terraform-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
4
|
+
codeterrain_terraform-0.2.0.dist-info/entry_points.txt,sha256=plsI_u-6DJsJNCzEqS1ZztnNU5lcfR3Tz6Db3BG9k9o,67
|
|
5
|
+
codeterrain_terraform-0.2.0.dist-info/RECORD,,
|