cfn-check 0.3.2__py3-none-any.whl → 0.8.1__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.
Potentially problematic release.
This version of cfn-check might be problematic. Click here for more details.
- cfn_check/cli/config.py +10 -0
- cfn_check/cli/render.py +142 -0
- cfn_check/cli/root.py +3 -1
- cfn_check/cli/utils/attributes.py +1 -1
- cfn_check/cli/utils/files.py +46 -21
- cfn_check/cli/utils/stdout.py +18 -0
- cfn_check/cli/validate.py +35 -26
- cfn_check/collection/collection.py +58 -1
- cfn_check/evaluation/evaluator.py +31 -3
- cfn_check/evaluation/parsing/token.py +4 -1
- cfn_check/evaluation/validate.py +33 -2
- cfn_check/rendering/__init__.py +1 -0
- cfn_check/rendering/cidr_solver.py +66 -0
- cfn_check/rendering/renderer.py +1316 -0
- cfn_check/rendering/utils.py +13 -0
- cfn_check/rules/rule.py +3 -0
- cfn_check/validation/validator.py +11 -1
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/METADATA +106 -5
- cfn_check-0.8.1.dist-info/RECORD +42 -0
- example/multitag.py +21 -0
- example/pydantic_rules.py +114 -0
- example/renderer_test.py +42 -0
- cfn_check/loader/__init__.py +0 -0
- cfn_check/loader/loader.py +0 -21
- cfn_check-0.3.2.dist-info/RECORD +0 -34
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/WHEEL +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Callable, Any
|
|
6
|
+
from collections import deque
|
|
7
|
+
from ruamel.yaml.tag import Tag
|
|
8
|
+
from ruamel.yaml.comments import TaggedScalar, CommentedMap, CommentedSeq
|
|
9
|
+
from .cidr_solver import IPv4CIDRSolver
|
|
10
|
+
from .utils import assign
|
|
11
|
+
|
|
12
|
+
from cfn_check.shared.types import (
|
|
13
|
+
Data,
|
|
14
|
+
Items,
|
|
15
|
+
YamlObject,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
Resolver = Callable[
|
|
19
|
+
[
|
|
20
|
+
CommentedMap,
|
|
21
|
+
CommentedMap | CommentedSeq | TaggedScalar | YamlObject
|
|
22
|
+
],
|
|
23
|
+
CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
class Renderer:
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.items: Items = deque()
|
|
30
|
+
self._sub_pattern = re.compile(r'\$\{([\w+::]+)\}')
|
|
31
|
+
self._sub_inner_text_pattern = re.compile(r'[\$|\{|\}]+')
|
|
32
|
+
self._visited: list[str | int] = []
|
|
33
|
+
self._data: YamlObject = {}
|
|
34
|
+
self._parameters = CommentedMap()
|
|
35
|
+
self._mappings = CommentedMap()
|
|
36
|
+
self._parameters_with_defaults: dict[str, str | int | float | bool | None] = {}
|
|
37
|
+
self._selected_mappings = CommentedMap()
|
|
38
|
+
self._conditions = CommentedMap()
|
|
39
|
+
self._references: dict[str, str] = {}
|
|
40
|
+
self._resources: dict[str, YamlObject] = CommentedMap()
|
|
41
|
+
self._attributes: dict[str, str] = {}
|
|
42
|
+
self._import_values: dict[
|
|
43
|
+
str,
|
|
44
|
+
CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
45
|
+
] = {}
|
|
46
|
+
self._availability_zones = CommentedSeq()
|
|
47
|
+
|
|
48
|
+
self._inline_functions = {
|
|
49
|
+
'Fn::ForEach': re.compile(r'Fn::ForEach::\w+'),
|
|
50
|
+
'Fn::If': re.compile(r'Fn::If'),
|
|
51
|
+
'Fn::And': re.compile(r'Fn::And'),
|
|
52
|
+
'Fn::Equals': re.compile(r'Fn::Equals'),
|
|
53
|
+
'Fn::Not': re.compile(r'Fn::Not'),
|
|
54
|
+
'Fn::Or': re.compile(r'Fn::Or'),
|
|
55
|
+
'Fn:GetAtt': re.compile(r'Fn::GetAtt'),
|
|
56
|
+
'Fn::Join': re.compile(r'Fn::Join'),
|
|
57
|
+
'Fn::Sub': re.compile(r'Fn::Sub'),
|
|
58
|
+
'Fn::Base64': re.compile(r'Fn::Base64'),
|
|
59
|
+
'Fn::Split': re.compile(r'Fn::Split'),
|
|
60
|
+
'Fn::Select': re.compile(r'Fn::Select'),
|
|
61
|
+
'Fn::ToJsonString': re.compile(r'Fn::ToJsonString'),
|
|
62
|
+
'Fn::Condition': re.compile(r'Fn::Condition'),
|
|
63
|
+
'Fn::Cidr': re.compile(r'Fn::Cidr'),
|
|
64
|
+
'Fn::Length': re.compile(r'Fn::Length'),
|
|
65
|
+
'Fn::GetAZs': re.compile(r'Fn::GetAZs'),
|
|
66
|
+
'Fn::ImportValue': re.compile(r'Fn::ImportValue'),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
self._inline_resolvers = {
|
|
70
|
+
'Fn::ForEach': self._resolve_foreach,
|
|
71
|
+
'Fn::If': self._resolve_if,
|
|
72
|
+
'Fn::And': self._resolve_and,
|
|
73
|
+
'Fn::Equals': self._resolve_equals,
|
|
74
|
+
'Fn::Not': self._resolve_not,
|
|
75
|
+
'Fn::Or': self._resolve_or,
|
|
76
|
+
'Fn:GetAtt': self._resolve_getatt,
|
|
77
|
+
'Fn::Join': self._resolve_join,
|
|
78
|
+
'Fn::Sub': self._resolve_sub,
|
|
79
|
+
'Fn::Base64': self._resolve_base64,
|
|
80
|
+
'Fn::Split': self._resolve_split,
|
|
81
|
+
'Fn::Select': self._resolve_select,
|
|
82
|
+
'Fn::ToJsonString': self._resolve_tree_to_json,
|
|
83
|
+
'Fn::Condition': self._resolve_condition,
|
|
84
|
+
'Fn::Cidr': self._resolve_cidr,
|
|
85
|
+
'Fn::Length': self._resolve_length,
|
|
86
|
+
'Fn::GetAZs': self._resolve_get_availability_zones,
|
|
87
|
+
'Fn::ImportValue': self._resolve_import_value,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self._resolvers: dict[str, Callable[[CommentedMap, str], YamlObject]] = {
|
|
91
|
+
'!Ref': self._resolve_ref,
|
|
92
|
+
'!FindInMap': self._resolve_by_subset_query,
|
|
93
|
+
'!GetAtt': self._resolve_getatt,
|
|
94
|
+
'!Join': self._resolve_join,
|
|
95
|
+
'!Sub': self._resolve_sub,
|
|
96
|
+
'!Base64': self._resolve_base64,
|
|
97
|
+
'!Split': self._resolve_split,
|
|
98
|
+
'!Select': self._resolve_select,
|
|
99
|
+
'!ToJsonString': self._resolve_tree_to_json,
|
|
100
|
+
'!Equals': self._resolve_equals,
|
|
101
|
+
'!If': self._resolve_if,
|
|
102
|
+
'!Condition': self._resolve_condition,
|
|
103
|
+
'!And': self._resolve_and,
|
|
104
|
+
'!Not': self._resolve_not,
|
|
105
|
+
'!Or': self._resolve_or,
|
|
106
|
+
'!Cidr': self._resolve_cidr,
|
|
107
|
+
'!GetAZs': self._resolve_get_availability_zones,
|
|
108
|
+
'!ImportValue': self._resolve_import_value,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def render(
|
|
112
|
+
self,
|
|
113
|
+
template: YamlObject,
|
|
114
|
+
attributes: dict[str, Any] | None = None,
|
|
115
|
+
availability_zones: list[str] | None = None,
|
|
116
|
+
import_values: dict[str, tuple[str, CommentedMap]] | None = None,
|
|
117
|
+
mappings: dict[str, str] | None = None,
|
|
118
|
+
parameters: dict[str, Any] | None = None,
|
|
119
|
+
references: dict[str, str] | None = None,
|
|
120
|
+
):
|
|
121
|
+
|
|
122
|
+
self._sources = list(template.keys())
|
|
123
|
+
|
|
124
|
+
self._assemble_parameters(template)
|
|
125
|
+
|
|
126
|
+
if attributes:
|
|
127
|
+
self._attributes = self._process_attributes(attributes)
|
|
128
|
+
|
|
129
|
+
if availability_zones:
|
|
130
|
+
self._availability_zones = CommentedSeq(availability_zones)
|
|
131
|
+
|
|
132
|
+
if import_values:
|
|
133
|
+
for _, (import_key, imported_template) in import_values.items():
|
|
134
|
+
self._import_values[import_key] = self._resolve_external_export(import_key, imported_template)
|
|
135
|
+
|
|
136
|
+
self._mappings = template.get('Mappings', CommentedMap())
|
|
137
|
+
if mappings:
|
|
138
|
+
self._selected_mappings = self._assemble_mappings(mappings)
|
|
139
|
+
|
|
140
|
+
self._parameters = template.get('Parameters', CommentedMap())
|
|
141
|
+
if parameters:
|
|
142
|
+
self._parameters_with_defaults.update(parameters)
|
|
143
|
+
|
|
144
|
+
if references:
|
|
145
|
+
self._references.update(references)
|
|
146
|
+
|
|
147
|
+
self._resources = template.get('Resources', CommentedMap())
|
|
148
|
+
self._conditions = template.get('Conditions', CommentedMap())
|
|
149
|
+
|
|
150
|
+
return self._resolve_tree(template)
|
|
151
|
+
|
|
152
|
+
def _resolve_tree(self, root: YamlObject):
|
|
153
|
+
self.items.clear()
|
|
154
|
+
self.items.append((None, None, root))
|
|
155
|
+
|
|
156
|
+
while self.items:
|
|
157
|
+
parent, accessor, node = self.items.pop()
|
|
158
|
+
if match := self._match_and_resolve_accessor_fn(
|
|
159
|
+
root,
|
|
160
|
+
parent,
|
|
161
|
+
accessor,
|
|
162
|
+
node,
|
|
163
|
+
):
|
|
164
|
+
root.update(match)
|
|
165
|
+
|
|
166
|
+
if isinstance(node, TaggedScalar):
|
|
167
|
+
# Replace in parent
|
|
168
|
+
if parent is not None and (
|
|
169
|
+
resolved := self._resolve_tagged(root, node)
|
|
170
|
+
):
|
|
171
|
+
parent[accessor] = resolved
|
|
172
|
+
|
|
173
|
+
elif isinstance(node, CommentedMap):
|
|
174
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent:
|
|
175
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
176
|
+
parent[accessor] = resolved_node
|
|
177
|
+
|
|
178
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None:
|
|
179
|
+
node = self._resolve_tagged(root, node)
|
|
180
|
+
for k in reversed(list(node.keys())):
|
|
181
|
+
self.items.append((node, k, node[k]))
|
|
182
|
+
|
|
183
|
+
root = node
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
# Process keys in reverse order for proper DFS
|
|
187
|
+
for k in reversed(list(node.keys())):
|
|
188
|
+
self.items.append((node, k, node[k]))
|
|
189
|
+
|
|
190
|
+
elif isinstance(node, CommentedSeq):
|
|
191
|
+
|
|
192
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent:
|
|
193
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
194
|
+
parent[accessor] = resolved_node
|
|
195
|
+
|
|
196
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None:
|
|
197
|
+
node = self._resolve_tagged(root, node)
|
|
198
|
+
|
|
199
|
+
for idx, val in enumerate(reversed(node)):
|
|
200
|
+
self.items.append((node, idx, val))
|
|
201
|
+
|
|
202
|
+
root = node
|
|
203
|
+
|
|
204
|
+
else:
|
|
205
|
+
# Process indices in reverse order for proper DFS
|
|
206
|
+
for idx, val in enumerate(reversed(node)):
|
|
207
|
+
self.items.append((node, idx, val))
|
|
208
|
+
|
|
209
|
+
return root
|
|
210
|
+
|
|
211
|
+
def _match_and_resolve_accessor_fn(
|
|
212
|
+
self,
|
|
213
|
+
root: CommentedMap,
|
|
214
|
+
parent: CommentedMap | CommentedSeq | TaggedScalar | YamlObject | None,
|
|
215
|
+
accessor: str | int | None,
|
|
216
|
+
node: CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
217
|
+
):
|
|
218
|
+
if not isinstance(accessor, str):
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
resolver: Resolver | None = None
|
|
222
|
+
matcher_pattern: re.Pattern | None = None
|
|
223
|
+
for key, pattern in self._inline_functions.items():
|
|
224
|
+
if pattern.match(accessor):
|
|
225
|
+
matcher_pattern = pattern
|
|
226
|
+
resolver = self._inline_resolvers[key]
|
|
227
|
+
|
|
228
|
+
if resolver is None:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
result = resolver(root, node)
|
|
232
|
+
|
|
233
|
+
return self._replace_target(
|
|
234
|
+
root,
|
|
235
|
+
parent,
|
|
236
|
+
result,
|
|
237
|
+
matcher_pattern,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _resolve_tagged(self, root: CommentedMap, node: TaggedScalar | CommentedMap | CommentedSeq):
|
|
241
|
+
resolver: Callable[[CommentedMap, str], YamlObject] | None = None
|
|
242
|
+
|
|
243
|
+
if isinstance(node.tag, Tag) and (
|
|
244
|
+
resolver := self._resolvers.get(node.tag.value)
|
|
245
|
+
):
|
|
246
|
+
return resolver(root, node)
|
|
247
|
+
|
|
248
|
+
def _resolve_ref(self, root: YamlObject, scalar: TaggedScalar):
|
|
249
|
+
'''
|
|
250
|
+
Sometimes we can resolve a !Ref if it has an explicit correlation
|
|
251
|
+
to a Resources key or input Parameter. This helps reduce the amount
|
|
252
|
+
of work we have to do when resolving later.
|
|
253
|
+
'''
|
|
254
|
+
if val := self._parameters_with_defaults.get(scalar.value):
|
|
255
|
+
return val
|
|
256
|
+
|
|
257
|
+
elif scalar.value in self._parameters:
|
|
258
|
+
return scalar
|
|
259
|
+
|
|
260
|
+
elif scalar.value in self._resources:
|
|
261
|
+
return scalar.value
|
|
262
|
+
|
|
263
|
+
elif ref := self._references.get(scalar.value):
|
|
264
|
+
return ref
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
return self._resolve_subtree(
|
|
268
|
+
root,
|
|
269
|
+
self._find_matching_key(root, scalar.value),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _resolve_getatt(
|
|
273
|
+
self,
|
|
274
|
+
root: CommentedMap,
|
|
275
|
+
query: TaggedScalar | CommentedMap | CommentedSeq,
|
|
276
|
+
) -> YamlObject | None:
|
|
277
|
+
steps: list[str] = []
|
|
278
|
+
|
|
279
|
+
if isinstance(query, TaggedScalar):
|
|
280
|
+
steps_string: str = query.value
|
|
281
|
+
steps = steps_string.split('.')
|
|
282
|
+
|
|
283
|
+
elif (
|
|
284
|
+
resolved := self._resolve_subtree(root, query)
|
|
285
|
+
) and isinstance(
|
|
286
|
+
resolved,
|
|
287
|
+
CommentedSeq,
|
|
288
|
+
):
|
|
289
|
+
steps = resolved
|
|
290
|
+
|
|
291
|
+
if value := self._attributes.get(
|
|
292
|
+
'.'.join(steps)
|
|
293
|
+
):
|
|
294
|
+
return value
|
|
295
|
+
|
|
296
|
+
current = self._resources.get(steps[0], CommentedMap()).get(
|
|
297
|
+
'Properties',
|
|
298
|
+
CommentedMap(),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
for step in steps[1:]:
|
|
302
|
+
if step == 'Value':
|
|
303
|
+
return current
|
|
304
|
+
# Mapping
|
|
305
|
+
if isinstance(current, (CommentedMap, dict)):
|
|
306
|
+
if step in current:
|
|
307
|
+
current = current[step]
|
|
308
|
+
else:
|
|
309
|
+
return None
|
|
310
|
+
# Sequence
|
|
311
|
+
elif isinstance(current, (CommentedSeq, list)):
|
|
312
|
+
try:
|
|
313
|
+
idx = int(step)
|
|
314
|
+
except ValueError:
|
|
315
|
+
return None
|
|
316
|
+
if 0 <= idx < len(current):
|
|
317
|
+
current = current[idx]
|
|
318
|
+
else:
|
|
319
|
+
return None
|
|
320
|
+
else:
|
|
321
|
+
# Hit a scalar (including TaggedScalar) before consuming all steps
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
return current
|
|
325
|
+
|
|
326
|
+
def _resolve_join(
|
|
327
|
+
self,
|
|
328
|
+
root: CommentedMap,
|
|
329
|
+
source: CommentedSeq,
|
|
330
|
+
) -> Any:
|
|
331
|
+
if len(source) < 2:
|
|
332
|
+
return ''
|
|
333
|
+
|
|
334
|
+
delimiter = source[0]
|
|
335
|
+
if isinstance(delimiter, (TaggedScalar, CommentedMap, CommentedSeq)):
|
|
336
|
+
delimiter = str(self._resolve_tagged(root, delimiter))
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
delimiter = str(delimiter)
|
|
340
|
+
|
|
341
|
+
subselction = source[1:]
|
|
342
|
+
resolved = self._resolve_subtree(root, subselction)
|
|
343
|
+
|
|
344
|
+
if not isinstance(resolved, CommentedSeq):
|
|
345
|
+
return source
|
|
346
|
+
|
|
347
|
+
return delimiter.join([
|
|
348
|
+
str(self._resolve_tagged(
|
|
349
|
+
root,
|
|
350
|
+
node,
|
|
351
|
+
))
|
|
352
|
+
if isinstance(
|
|
353
|
+
node,
|
|
354
|
+
(TaggedScalar, CommentedMap, CommentedSeq)
|
|
355
|
+
) else node
|
|
356
|
+
for subset in resolved
|
|
357
|
+
for node in subset
|
|
358
|
+
])
|
|
359
|
+
|
|
360
|
+
def _resolve_sub(
|
|
361
|
+
self,
|
|
362
|
+
root: CommentedMap,
|
|
363
|
+
source: CommentedSeq | TaggedScalar,
|
|
364
|
+
):
|
|
365
|
+
if isinstance(source, TaggedScalar) and isinstance(
|
|
366
|
+
source.tag,
|
|
367
|
+
Tag,
|
|
368
|
+
):
|
|
369
|
+
source_string = source.value
|
|
370
|
+
variables = self._resolve_template_string(source_string)
|
|
371
|
+
return self._resolve_sub_ref_queries(
|
|
372
|
+
variables,
|
|
373
|
+
source_string,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
elif len(source) > 1:
|
|
377
|
+
source_string: str = source[0]
|
|
378
|
+
template_vars = self._resolve_template_string(source_string)
|
|
379
|
+
variables = source[1:]
|
|
380
|
+
resolved: list[dict[str, Any]] = self._resolve_subtree(root, variables)
|
|
381
|
+
|
|
382
|
+
for resolve_var in resolved:
|
|
383
|
+
for template_var, accessor in template_vars:
|
|
384
|
+
if val := resolve_var.get(accessor):
|
|
385
|
+
source_string = source_string.replace(template_var, val)
|
|
386
|
+
|
|
387
|
+
return source_string
|
|
388
|
+
|
|
389
|
+
return source
|
|
390
|
+
|
|
391
|
+
def _resolve_base64(
|
|
392
|
+
self,
|
|
393
|
+
root: CommentedMap,
|
|
394
|
+
source: CommentedMap | CommentedSeq | TaggedScalar,
|
|
395
|
+
):
|
|
396
|
+
if isinstance(source, TaggedScalar) and isinstance(
|
|
397
|
+
source.tag,
|
|
398
|
+
Tag,
|
|
399
|
+
) and isinstance(
|
|
400
|
+
source.tag.value,
|
|
401
|
+
str,
|
|
402
|
+
):
|
|
403
|
+
return base64.b64encode(source.tag.value.encode()).decode('ascii')
|
|
404
|
+
|
|
405
|
+
elif (
|
|
406
|
+
resolved := self._resolve_subtree(root, source)
|
|
407
|
+
) and isinstance(
|
|
408
|
+
resolved,
|
|
409
|
+
str
|
|
410
|
+
):
|
|
411
|
+
return base64.b64encode(resolved.encode()).decode('ascii')
|
|
412
|
+
|
|
413
|
+
return source
|
|
414
|
+
|
|
415
|
+
def _resolve_foreach(
|
|
416
|
+
self,
|
|
417
|
+
root: CommentedMap,
|
|
418
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
419
|
+
):
|
|
420
|
+
if not isinstance(source, CommentedSeq) or len(source) < 3:
|
|
421
|
+
return source
|
|
422
|
+
|
|
423
|
+
identifier = source[0]
|
|
424
|
+
if not isinstance(identifier, str):
|
|
425
|
+
identifier = self._resolve_subtree(root, identifier)
|
|
426
|
+
|
|
427
|
+
collection = source[1]
|
|
428
|
+
if not isinstance(collection, list):
|
|
429
|
+
return source
|
|
430
|
+
|
|
431
|
+
collection: list[str] = self._resolve_subtree(root, collection)
|
|
432
|
+
|
|
433
|
+
output = source[2]
|
|
434
|
+
if not isinstance(output, CommentedMap):
|
|
435
|
+
return source
|
|
436
|
+
|
|
437
|
+
resolved_items = CommentedMap()
|
|
438
|
+
for item in collection:
|
|
439
|
+
self._references[identifier] = item
|
|
440
|
+
resolved_items.update(
|
|
441
|
+
self._resolve_foreach_item(
|
|
442
|
+
root,
|
|
443
|
+
self._copy_subtree(output),
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return resolved_items
|
|
448
|
+
|
|
449
|
+
def _resolve_foreach_item(
|
|
450
|
+
self,
|
|
451
|
+
root: CommentedMap,
|
|
452
|
+
output_item: CommentedMap,
|
|
453
|
+
):
|
|
454
|
+
output_map: dict[str, CommentedMap] = {}
|
|
455
|
+
for output_key, output_value in output_item.items():
|
|
456
|
+
variables = self._resolve_template_string(output_key)
|
|
457
|
+
resolved_key = self._resolve_sub_ref_queries(
|
|
458
|
+
variables,
|
|
459
|
+
output_key,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
output_map[resolved_key] = self._resolve_subtree(
|
|
463
|
+
root,
|
|
464
|
+
output_value,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
return output_map
|
|
468
|
+
|
|
469
|
+
def _resolve_split(
|
|
470
|
+
self,
|
|
471
|
+
root: CommentedMap,
|
|
472
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
473
|
+
):
|
|
474
|
+
if isinstance(
|
|
475
|
+
source,
|
|
476
|
+
(CommentedMap, TaggedScalar),
|
|
477
|
+
) or len(source) != 2:
|
|
478
|
+
return source
|
|
479
|
+
|
|
480
|
+
delimiter = source[0]
|
|
481
|
+
if not isinstance(
|
|
482
|
+
delimiter,
|
|
483
|
+
str,
|
|
484
|
+
):
|
|
485
|
+
delimiter = self._resolve_subtree(root, delimiter)
|
|
486
|
+
|
|
487
|
+
target = source[1]
|
|
488
|
+
if not isinstance(
|
|
489
|
+
target,
|
|
490
|
+
str,
|
|
491
|
+
):
|
|
492
|
+
target = self._resolve_subtree(root, target)
|
|
493
|
+
|
|
494
|
+
if isinstance(delimiter, str) and isinstance(target, str):
|
|
495
|
+
return CommentedSeq(target.split(delimiter))
|
|
496
|
+
|
|
497
|
+
return target
|
|
498
|
+
|
|
499
|
+
def _resolve_select(
|
|
500
|
+
self,
|
|
501
|
+
root: CommentedMap,
|
|
502
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
503
|
+
):
|
|
504
|
+
if isinstance(
|
|
505
|
+
source,
|
|
506
|
+
(CommentedMap, TaggedScalar),
|
|
507
|
+
) or len(source) != 2:
|
|
508
|
+
return source
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
index = source[0]
|
|
512
|
+
if not isinstance(
|
|
513
|
+
index,
|
|
514
|
+
int,
|
|
515
|
+
):
|
|
516
|
+
index = self._resolve_subtree(root, index)
|
|
517
|
+
|
|
518
|
+
target = self._resolve_subtree(root, source[1])
|
|
519
|
+
if index > len(target):
|
|
520
|
+
return source
|
|
521
|
+
|
|
522
|
+
return target[index]
|
|
523
|
+
|
|
524
|
+
def _resolve_equals(
|
|
525
|
+
self,
|
|
526
|
+
root: CommentedMap,
|
|
527
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
528
|
+
):
|
|
529
|
+
if isinstance(
|
|
530
|
+
source,
|
|
531
|
+
(CommentedMap, TaggedScalar),
|
|
532
|
+
) or len(source) != 2:
|
|
533
|
+
return source
|
|
534
|
+
|
|
535
|
+
item_a = source[0]
|
|
536
|
+
if isinstance(
|
|
537
|
+
item_a,
|
|
538
|
+
(CommentedMap, CommentedSeq, TaggedScalar),
|
|
539
|
+
):
|
|
540
|
+
item_a = self._resolve_subtree(root, item_a)
|
|
541
|
+
|
|
542
|
+
item_b = source[1]
|
|
543
|
+
if isinstance(
|
|
544
|
+
item_b,
|
|
545
|
+
(CommentedMap, CommentedSeq, TaggedScalar),
|
|
546
|
+
):
|
|
547
|
+
item_b = self._resolve_subtree(root, item_b)
|
|
548
|
+
|
|
549
|
+
return item_a == item_b
|
|
550
|
+
|
|
551
|
+
def _resolve_if(
|
|
552
|
+
self,
|
|
553
|
+
root: CommentedMap,
|
|
554
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
555
|
+
):
|
|
556
|
+
if isinstance(
|
|
557
|
+
source,
|
|
558
|
+
(CommentedMap, TaggedScalar),
|
|
559
|
+
) or len(source) != 3:
|
|
560
|
+
return source
|
|
561
|
+
|
|
562
|
+
condition_key = source[0]
|
|
563
|
+
if isinstance(
|
|
564
|
+
condition_key,
|
|
565
|
+
(CommentedMap, CommentedSeq, TaggedScalar),
|
|
566
|
+
):
|
|
567
|
+
condition_key = self._resolve_subtree(root, condition_key)
|
|
568
|
+
|
|
569
|
+
result = self._resolve_subtree(root, self._conditions.get(condition_key))
|
|
570
|
+
|
|
571
|
+
true_result = source[1]
|
|
572
|
+
if isinstance(
|
|
573
|
+
true_result,
|
|
574
|
+
(CommentedMap, CommentedSeq, TaggedScalar),
|
|
575
|
+
):
|
|
576
|
+
true_result = self._resolve_subtree(root, true_result)
|
|
577
|
+
|
|
578
|
+
false_result = source[2]
|
|
579
|
+
|
|
580
|
+
return true_result if isinstance(result, bool) and result else false_result
|
|
581
|
+
|
|
582
|
+
def _resolve_condition(
|
|
583
|
+
self,
|
|
584
|
+
root: CommentedMap,
|
|
585
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
586
|
+
):
|
|
587
|
+
if isinstance(
|
|
588
|
+
source,
|
|
589
|
+
(CommentedMap, CommentedSeq),
|
|
590
|
+
):
|
|
591
|
+
return source
|
|
592
|
+
|
|
593
|
+
if (
|
|
594
|
+
condition := self._conditions.get(source.value)
|
|
595
|
+
) and isinstance(
|
|
596
|
+
condition,
|
|
597
|
+
(CommentedMap, CommentedSeq, TaggedScalar)
|
|
598
|
+
) and (
|
|
599
|
+
result := self._resolve_subtree(root, condition)
|
|
600
|
+
) and isinstance(
|
|
601
|
+
result,
|
|
602
|
+
bool,
|
|
603
|
+
):
|
|
604
|
+
return result
|
|
605
|
+
|
|
606
|
+
elif (
|
|
607
|
+
condition := self._conditions.get(source.value)
|
|
608
|
+
) and isinstance(
|
|
609
|
+
condition,
|
|
610
|
+
bool,
|
|
611
|
+
):
|
|
612
|
+
return condition
|
|
613
|
+
|
|
614
|
+
return source
|
|
615
|
+
|
|
616
|
+
def _resolve_and(
|
|
617
|
+
self,
|
|
618
|
+
root: CommentedMap,
|
|
619
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
620
|
+
):
|
|
621
|
+
if isinstance(
|
|
622
|
+
source,
|
|
623
|
+
(CommentedMap, TaggedScalar),
|
|
624
|
+
):
|
|
625
|
+
return source
|
|
626
|
+
|
|
627
|
+
resolved = self._resolve_subtree(root, CommentedSeq([
|
|
628
|
+
item for item in source
|
|
629
|
+
]))
|
|
630
|
+
if not isinstance(resolved, CommentedSeq):
|
|
631
|
+
return source
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
for node in resolved:
|
|
635
|
+
if not isinstance(node, bool):
|
|
636
|
+
return source
|
|
637
|
+
|
|
638
|
+
return all(resolved)
|
|
639
|
+
|
|
640
|
+
def _resolve_not(
|
|
641
|
+
self,
|
|
642
|
+
root: CommentedMap,
|
|
643
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
644
|
+
):
|
|
645
|
+
if isinstance(
|
|
646
|
+
source,
|
|
647
|
+
(CommentedMap, TaggedScalar),
|
|
648
|
+
):
|
|
649
|
+
return source
|
|
650
|
+
|
|
651
|
+
resolved = self._resolve_subtree(root, CommentedSeq([
|
|
652
|
+
item for item in source
|
|
653
|
+
]))
|
|
654
|
+
if not isinstance(resolved, CommentedSeq):
|
|
655
|
+
return source
|
|
656
|
+
|
|
657
|
+
for node in resolved:
|
|
658
|
+
if not isinstance(node, bool):
|
|
659
|
+
return source
|
|
660
|
+
|
|
661
|
+
return not all(resolved)
|
|
662
|
+
|
|
663
|
+
def _resolve_or(
|
|
664
|
+
self,
|
|
665
|
+
root: CommentedMap,
|
|
666
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
667
|
+
):
|
|
668
|
+
if isinstance(
|
|
669
|
+
source,
|
|
670
|
+
(CommentedMap, TaggedScalar),
|
|
671
|
+
):
|
|
672
|
+
return source
|
|
673
|
+
|
|
674
|
+
resolved = self._resolve_subtree(root, CommentedSeq([
|
|
675
|
+
item for item in source
|
|
676
|
+
]))
|
|
677
|
+
if not isinstance(resolved, CommentedSeq):
|
|
678
|
+
return source
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
for node in resolved:
|
|
682
|
+
if not isinstance(node, bool):
|
|
683
|
+
return source
|
|
684
|
+
|
|
685
|
+
return any(resolved)
|
|
686
|
+
|
|
687
|
+
def _resolve_cidr(
|
|
688
|
+
self,
|
|
689
|
+
root: CommentedMap,
|
|
690
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
691
|
+
):
|
|
692
|
+
if not isinstance(
|
|
693
|
+
source,
|
|
694
|
+
CommentedSeq,
|
|
695
|
+
) or len(source) < 3:
|
|
696
|
+
return source
|
|
697
|
+
|
|
698
|
+
cidr = self._resolve_subtree(root, source[0])
|
|
699
|
+
if not isinstance(cidr, str):
|
|
700
|
+
return source
|
|
701
|
+
|
|
702
|
+
subnets_requested = source[1]
|
|
703
|
+
subnet_cidr_bits = source[2]
|
|
704
|
+
|
|
705
|
+
ipv4_solver = IPv4CIDRSolver(
|
|
706
|
+
cidr,
|
|
707
|
+
subnets_requested,
|
|
708
|
+
subnet_cidr_bits,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
return CommentedSeq(ipv4_solver.provision_subnets())
|
|
712
|
+
|
|
713
|
+
def _resolve_tree_to_json(
|
|
714
|
+
self,
|
|
715
|
+
root: CommentedMap,
|
|
716
|
+
source: CommentedSeq | CommentedMap | TaggedScalar,
|
|
717
|
+
):
|
|
718
|
+
|
|
719
|
+
stack: list[tuple[CommentedMap | CommentedSeq | None, Any | None, Any]] = [(None, None, source)]
|
|
720
|
+
|
|
721
|
+
while stack:
|
|
722
|
+
parent, accessor, node = stack.pop()
|
|
723
|
+
if isinstance(node, TaggedScalar):
|
|
724
|
+
# Replace in parent
|
|
725
|
+
if parent is not None and (
|
|
726
|
+
resolved := self._resolve_tagged(root, node)
|
|
727
|
+
):
|
|
728
|
+
parent[accessor] = resolved
|
|
729
|
+
|
|
730
|
+
elif (
|
|
731
|
+
resolved := self._resolve_tagged(root, node)
|
|
732
|
+
):
|
|
733
|
+
source = resolved
|
|
734
|
+
|
|
735
|
+
elif isinstance(node, CommentedMap):
|
|
736
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent and (
|
|
737
|
+
resolved_node := self._resolve_tagged(root, node)
|
|
738
|
+
) and node != source:
|
|
739
|
+
parent[accessor] = resolved_node
|
|
740
|
+
|
|
741
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None and node != source:
|
|
742
|
+
node = self._resolve_tagged(root, node)
|
|
743
|
+
for k in reversed(list(node.keys())):
|
|
744
|
+
stack.append((node, k, node[k]))
|
|
745
|
+
|
|
746
|
+
source = node
|
|
747
|
+
|
|
748
|
+
else:
|
|
749
|
+
# Push children (keys) in reverse for DFS order
|
|
750
|
+
for k in reversed(list(node.keys())):
|
|
751
|
+
stack.append((node, k, node[k]))
|
|
752
|
+
|
|
753
|
+
elif isinstance(node, CommentedSeq):
|
|
754
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent and (
|
|
755
|
+
resolved_node := self._resolve_tagged(root, node)
|
|
756
|
+
) and node != source :
|
|
757
|
+
parent[accessor] = resolved_node
|
|
758
|
+
|
|
759
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None and node != source:
|
|
760
|
+
node = self._resolve_tagged(root, node)
|
|
761
|
+
for idx, val in enumerate(reversed(node)):
|
|
762
|
+
stack.append((node, idx, val))
|
|
763
|
+
|
|
764
|
+
source = node
|
|
765
|
+
|
|
766
|
+
else:
|
|
767
|
+
# Process indices in reverse order for proper DFS
|
|
768
|
+
for idx, val in enumerate(reversed(node)):
|
|
769
|
+
stack.append((node, idx, val))
|
|
770
|
+
|
|
771
|
+
return json.dumps(source)
|
|
772
|
+
|
|
773
|
+
def _resolve_length(
|
|
774
|
+
self,
|
|
775
|
+
root: CommentedMap,
|
|
776
|
+
source: CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
777
|
+
):
|
|
778
|
+
items = CommentedSeq()
|
|
779
|
+
if isinstance(source, TaggedScalar):
|
|
780
|
+
items = self._resolve_tagged(root, source)
|
|
781
|
+
|
|
782
|
+
elif isinstance(source, (CommentedMap, CommentedSeq)):
|
|
783
|
+
items = self._resolve_subtree(root, source)
|
|
784
|
+
|
|
785
|
+
elif isinstance(source, (str, list, dict)):
|
|
786
|
+
items = source
|
|
787
|
+
|
|
788
|
+
else:
|
|
789
|
+
return source
|
|
790
|
+
|
|
791
|
+
return len(items)
|
|
792
|
+
|
|
793
|
+
def _resolve_get_availability_zones(
|
|
794
|
+
self,
|
|
795
|
+
_: CommentedMap,
|
|
796
|
+
source: CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
797
|
+
):
|
|
798
|
+
if not isinstance(source, TaggedScalar):
|
|
799
|
+
return source
|
|
800
|
+
|
|
801
|
+
return self._availability_zones
|
|
802
|
+
|
|
803
|
+
def _resolve_import_value(
|
|
804
|
+
self,
|
|
805
|
+
_: CommentedMap,
|
|
806
|
+
source: CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
807
|
+
):
|
|
808
|
+
if not isinstance(source, TaggedScalar):
|
|
809
|
+
return source
|
|
810
|
+
|
|
811
|
+
return self._import_values.get(source.value)
|
|
812
|
+
|
|
813
|
+
def _resolve_external_export(
|
|
814
|
+
self,
|
|
815
|
+
key: str,
|
|
816
|
+
template: CommentedMap,
|
|
817
|
+
):
|
|
818
|
+
outputs: CommentedMap = template.get('Outputs', CommentedMap())
|
|
819
|
+
exports: CommentedMap = outputs.get('Export', CommentedMap())
|
|
820
|
+
|
|
821
|
+
if subtree := exports.get(key):
|
|
822
|
+
return self._resolve_subtree(template, subtree)
|
|
823
|
+
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
def _copy_subtree(
|
|
827
|
+
self,
|
|
828
|
+
root: CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
829
|
+
) -> Any:
|
|
830
|
+
"""
|
|
831
|
+
Depth-first clone of a ruamel.yaml tree.
|
|
832
|
+
- Rebuilds CommentedMap/CommentedSeq
|
|
833
|
+
- Copies TaggedScalar (preserves tag and value)
|
|
834
|
+
- Scalars are copied as-is
|
|
835
|
+
Note: does not preserve comments/anchors.
|
|
836
|
+
"""
|
|
837
|
+
if isinstance(root, CommentedMap):
|
|
838
|
+
root_clone: Any = CommentedMap()
|
|
839
|
+
elif isinstance(root, CommentedSeq):
|
|
840
|
+
root_clone = CommentedSeq()
|
|
841
|
+
elif isinstance(root, TaggedScalar):
|
|
842
|
+
return TaggedScalar(
|
|
843
|
+
value=root.value,
|
|
844
|
+
tag=root.tag,
|
|
845
|
+
)
|
|
846
|
+
else:
|
|
847
|
+
return root
|
|
848
|
+
|
|
849
|
+
stack: list[
|
|
850
|
+
tuple[
|
|
851
|
+
Any,
|
|
852
|
+
CommentedMap | CommentedSeq | None,
|
|
853
|
+
Any | None,
|
|
854
|
+
]
|
|
855
|
+
] = [(root, None, None)]
|
|
856
|
+
|
|
857
|
+
built: dict[
|
|
858
|
+
int,
|
|
859
|
+
CommentedMap | CommentedSeq,
|
|
860
|
+
] = {id(root): root_clone}
|
|
861
|
+
|
|
862
|
+
while stack:
|
|
863
|
+
in_node, out_parent, out_key = stack.pop()
|
|
864
|
+
|
|
865
|
+
if isinstance(in_node, CommentedMap):
|
|
866
|
+
out_container = built.get(id(in_node))
|
|
867
|
+
if out_container is None:
|
|
868
|
+
out_container = CommentedMap()
|
|
869
|
+
built[id(in_node)] = out_container
|
|
870
|
+
assign(out_parent, out_key, out_container)
|
|
871
|
+
|
|
872
|
+
for k in reversed(list(in_node.keys())):
|
|
873
|
+
v = in_node[k]
|
|
874
|
+
if isinstance(v, CommentedMap):
|
|
875
|
+
child = CommentedMap()
|
|
876
|
+
built[id(v)] = child
|
|
877
|
+
|
|
878
|
+
stack.append((v, out_container, k))
|
|
879
|
+
elif isinstance(v, CommentedSeq):
|
|
880
|
+
child = CommentedSeq()
|
|
881
|
+
built[id(v)] = child
|
|
882
|
+
|
|
883
|
+
stack.append((v, out_container, k))
|
|
884
|
+
elif isinstance(v, TaggedScalar):
|
|
885
|
+
ts = TaggedScalar(
|
|
886
|
+
value=v.value,
|
|
887
|
+
tag=v.tag,
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
out_container[k] = ts
|
|
891
|
+
else:
|
|
892
|
+
out_container[k] = v
|
|
893
|
+
|
|
894
|
+
elif isinstance(in_node, CommentedSeq):
|
|
895
|
+
out_container = built.get(id(in_node))
|
|
896
|
+
if out_container is None:
|
|
897
|
+
out_container = CommentedSeq()
|
|
898
|
+
built[id(in_node)] = out_container
|
|
899
|
+
assign(out_parent, out_key, out_container)
|
|
900
|
+
|
|
901
|
+
for idx in reversed(range(len(in_node))):
|
|
902
|
+
v = in_node[idx]
|
|
903
|
+
|
|
904
|
+
if isinstance(v, CommentedMap):
|
|
905
|
+
child = CommentedMap()
|
|
906
|
+
built[id(v)] = child
|
|
907
|
+
|
|
908
|
+
stack.append((v, out_container, idx))
|
|
909
|
+
elif isinstance(v, CommentedSeq):
|
|
910
|
+
child = CommentedSeq()
|
|
911
|
+
built[id(v)] = child
|
|
912
|
+
|
|
913
|
+
stack.append((v, out_container, idx))
|
|
914
|
+
elif isinstance(v, TaggedScalar):
|
|
915
|
+
ts = TaggedScalar(
|
|
916
|
+
value=v.value,
|
|
917
|
+
tag=v.tag,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
out_container.append(ts)
|
|
921
|
+
else:
|
|
922
|
+
out_container.append(v)
|
|
923
|
+
|
|
924
|
+
elif isinstance(in_node, TaggedScalar):
|
|
925
|
+
ts = TaggedScalar(
|
|
926
|
+
value=in_node.value,
|
|
927
|
+
tag=in_node.tag,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
assign(out_parent, out_key, ts)
|
|
931
|
+
|
|
932
|
+
else:
|
|
933
|
+
assign(out_parent, out_key, in_node)
|
|
934
|
+
|
|
935
|
+
return root_clone
|
|
936
|
+
|
|
937
|
+
def _replace_target(
|
|
938
|
+
self,
|
|
939
|
+
root: CommentedMap,
|
|
940
|
+
target: CommentedMap,
|
|
941
|
+
replacement: Any,
|
|
942
|
+
matcher_pattern: re.Pattern
|
|
943
|
+
) -> CommentedMap:
|
|
944
|
+
if not isinstance(target, CommentedMap):
|
|
945
|
+
return root
|
|
946
|
+
|
|
947
|
+
if root is target:
|
|
948
|
+
return replacement
|
|
949
|
+
|
|
950
|
+
stack: list[tuple[Any, Any | None, Any | None]] = [(root, None, None)]
|
|
951
|
+
|
|
952
|
+
while stack:
|
|
953
|
+
node, parent, accessor = stack.pop()
|
|
954
|
+
|
|
955
|
+
if isinstance(node, CommentedMap):
|
|
956
|
+
for k in reversed(list(node.keys())):
|
|
957
|
+
child = node[k]
|
|
958
|
+
if child is target and isinstance(child, CommentedMap):
|
|
959
|
+
for key in list(target.keys()):
|
|
960
|
+
if matcher_pattern.match(key):
|
|
961
|
+
del child[key]
|
|
962
|
+
|
|
963
|
+
if isinstance(replacement, CommentedMap):
|
|
964
|
+
child.update(replacement)
|
|
965
|
+
node[k] = child
|
|
966
|
+
|
|
967
|
+
else:
|
|
968
|
+
node[k] = replacement
|
|
969
|
+
|
|
970
|
+
if parent:
|
|
971
|
+
parent[accessor] = node
|
|
972
|
+
|
|
973
|
+
return root
|
|
974
|
+
|
|
975
|
+
stack.append((child, node, k))
|
|
976
|
+
|
|
977
|
+
elif isinstance(node, CommentedSeq):
|
|
978
|
+
for idx in reversed(range(len(node))):
|
|
979
|
+
child = node[idx]
|
|
980
|
+
if child is target and isinstance(child, CommentedMap):
|
|
981
|
+
for key in list(target.keys()):
|
|
982
|
+
if matcher_pattern.match(key):
|
|
983
|
+
del child[key]
|
|
984
|
+
|
|
985
|
+
if isinstance(replacement, CommentedMap):
|
|
986
|
+
child.update(replacement)
|
|
987
|
+
node[idx] = child
|
|
988
|
+
|
|
989
|
+
else:
|
|
990
|
+
node[idx] = replacement
|
|
991
|
+
|
|
992
|
+
if parent:
|
|
993
|
+
parent[accessor] = node
|
|
994
|
+
|
|
995
|
+
return root
|
|
996
|
+
|
|
997
|
+
stack.append((child, node, idx))
|
|
998
|
+
|
|
999
|
+
return root
|
|
1000
|
+
|
|
1001
|
+
def _resolve_subtree(
|
|
1002
|
+
self,
|
|
1003
|
+
root: CommentedMap,
|
|
1004
|
+
source: CommentedSeq
|
|
1005
|
+
) -> Any:
|
|
1006
|
+
"""
|
|
1007
|
+
Iterative DFS over a ruamel.yaml tree.
|
|
1008
|
+
- CommentedMap/CommentedSeq are traversed.
|
|
1009
|
+
"""
|
|
1010
|
+
stack: list[tuple[CommentedMap | CommentedSeq | None, Any | None, Any]] = [(None, None, source)]
|
|
1011
|
+
|
|
1012
|
+
source_parent, source_index = self._find_parent(root, source)
|
|
1013
|
+
|
|
1014
|
+
while stack:
|
|
1015
|
+
parent, accessor, node = stack.pop()
|
|
1016
|
+
if match := self._match_and_resolve_accessor_fn(
|
|
1017
|
+
root,
|
|
1018
|
+
parent,
|
|
1019
|
+
accessor,
|
|
1020
|
+
node,
|
|
1021
|
+
):
|
|
1022
|
+
root.update(match)
|
|
1023
|
+
# At this point we've likely (and completely)
|
|
1024
|
+
# successfully nuked the source from orbit
|
|
1025
|
+
# so we need to fetch it from the source parent
|
|
1026
|
+
# to get it back (i.e. the ref is no longer
|
|
1027
|
+
# correct).
|
|
1028
|
+
source = source_parent[source_index]
|
|
1029
|
+
|
|
1030
|
+
if isinstance(node, TaggedScalar):
|
|
1031
|
+
# Replace in parent
|
|
1032
|
+
if parent is not None and (
|
|
1033
|
+
resolved := self._resolve_tagged(root, node)
|
|
1034
|
+
):
|
|
1035
|
+
parent[accessor] = resolved
|
|
1036
|
+
|
|
1037
|
+
elif (
|
|
1038
|
+
resolved := self._resolve_tagged(root, node)
|
|
1039
|
+
):
|
|
1040
|
+
source = resolved
|
|
1041
|
+
|
|
1042
|
+
elif isinstance(node, CommentedMap):
|
|
1043
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent:
|
|
1044
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
1045
|
+
parent[accessor] = resolved_node
|
|
1046
|
+
|
|
1047
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None:
|
|
1048
|
+
node = self._resolve_tagged(root, node)
|
|
1049
|
+
for k in reversed(list(node.keys())):
|
|
1050
|
+
stack.append((node, k, node[k]))
|
|
1051
|
+
|
|
1052
|
+
source = node
|
|
1053
|
+
|
|
1054
|
+
else:
|
|
1055
|
+
# Push children (keys) in reverse for DFS order
|
|
1056
|
+
for k in reversed(list(node.keys())):
|
|
1057
|
+
stack.append((node, k, node[k]))
|
|
1058
|
+
|
|
1059
|
+
elif isinstance(node, CommentedSeq):
|
|
1060
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and parent:
|
|
1061
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
1062
|
+
parent[accessor] = resolved_node
|
|
1063
|
+
|
|
1064
|
+
elif isinstance(node.tag, Tag) and node.tag.value is not None:
|
|
1065
|
+
node = self._resolve_tagged(root, node)
|
|
1066
|
+
for idx, val in enumerate(reversed(node)):
|
|
1067
|
+
stack.append((node, idx, val))
|
|
1068
|
+
|
|
1069
|
+
source = node
|
|
1070
|
+
|
|
1071
|
+
else:
|
|
1072
|
+
# Process indices in reverse order for proper DFS
|
|
1073
|
+
for idx, val in enumerate(reversed(node)):
|
|
1074
|
+
stack.append((node, idx, val))
|
|
1075
|
+
|
|
1076
|
+
return source
|
|
1077
|
+
|
|
1078
|
+
def _resolve_by_subset_query(
|
|
1079
|
+
self,
|
|
1080
|
+
root: CommentedMap,
|
|
1081
|
+
subset: CommentedMap | CommentedSeq,
|
|
1082
|
+
) -> YamlObject | None:
|
|
1083
|
+
"""
|
|
1084
|
+
Traverse `subset` iteratively. For every leaf (scalar or TaggedScalar) encountered in `subset`,
|
|
1085
|
+
use its value as the next key/index into `root`. Return (path, value) where:
|
|
1086
|
+
- path: list of keys/indices used to reach into `root`
|
|
1087
|
+
- value: the value at the end of traversal, or None if a step was missing (early return)
|
|
1088
|
+
TaggedScalar is treated as a leaf and its .value is used as the key component.
|
|
1089
|
+
"""
|
|
1090
|
+
current = self._mappings
|
|
1091
|
+
path = []
|
|
1092
|
+
|
|
1093
|
+
stack = [(subset, [])]
|
|
1094
|
+
while stack:
|
|
1095
|
+
node, _ = stack.pop()
|
|
1096
|
+
|
|
1097
|
+
if isinstance(node, CommentedMap):
|
|
1098
|
+
|
|
1099
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and (
|
|
1100
|
+
node != subset
|
|
1101
|
+
):
|
|
1102
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
1103
|
+
stack.append((resolved_node, []))
|
|
1104
|
+
|
|
1105
|
+
else:
|
|
1106
|
+
for k in reversed(list(node.keys())):
|
|
1107
|
+
stack.append((node[k], []))
|
|
1108
|
+
|
|
1109
|
+
elif isinstance(node, CommentedSeq):
|
|
1110
|
+
|
|
1111
|
+
if isinstance(node.tag, Tag) and node.tag.value is not None and (
|
|
1112
|
+
node != subset
|
|
1113
|
+
):
|
|
1114
|
+
resolved_node = self._resolve_tagged(root, node)
|
|
1115
|
+
stack.append((resolved_node, []))
|
|
1116
|
+
|
|
1117
|
+
else:
|
|
1118
|
+
for val in reversed(node):
|
|
1119
|
+
stack.append((val, []))
|
|
1120
|
+
else:
|
|
1121
|
+
# Leaf: scalar or TaggedScalar
|
|
1122
|
+
key = self._resolve_tagged(
|
|
1123
|
+
self._selected_mappings,
|
|
1124
|
+
node,
|
|
1125
|
+
) if isinstance(node, TaggedScalar) else node
|
|
1126
|
+
path.append(key)
|
|
1127
|
+
|
|
1128
|
+
if isinstance(current, CommentedMap):
|
|
1129
|
+
if key in current:
|
|
1130
|
+
current = current[key]
|
|
1131
|
+
else:
|
|
1132
|
+
return None
|
|
1133
|
+
elif isinstance(current, CommentedSeq) and isinstance(key, int) and 0 <= key < len(current):
|
|
1134
|
+
current = current[key]
|
|
1135
|
+
else:
|
|
1136
|
+
return None
|
|
1137
|
+
|
|
1138
|
+
if isinstance(current, TaggedScalar):
|
|
1139
|
+
return path, self._resolve_tagged(
|
|
1140
|
+
self._selected_mappings,
|
|
1141
|
+
current,
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
return current
|
|
1145
|
+
|
|
1146
|
+
def _find_matching_key(
|
|
1147
|
+
self,
|
|
1148
|
+
root: CommentedMap,
|
|
1149
|
+
search_key: str,
|
|
1150
|
+
):
|
|
1151
|
+
"""Returns the first path (list of keys/indices) to a mapping with key == search_key, and the value at that path."""
|
|
1152
|
+
stack = [(root, [])]
|
|
1153
|
+
while stack:
|
|
1154
|
+
node, path = stack.pop()
|
|
1155
|
+
if isinstance(node, CommentedMap):
|
|
1156
|
+
for k in reversed(list(node.keys())):
|
|
1157
|
+
if k == search_key:
|
|
1158
|
+
return node[k]
|
|
1159
|
+
stack.append((node[k], path + [k]))
|
|
1160
|
+
elif isinstance(node, CommentedSeq):
|
|
1161
|
+
for idx, item in reversed(list(enumerate(node))):
|
|
1162
|
+
stack.append((item, path + [idx]))
|
|
1163
|
+
|
|
1164
|
+
return None # No match found
|
|
1165
|
+
|
|
1166
|
+
def _find_parent(
|
|
1167
|
+
self,
|
|
1168
|
+
root: CommentedMap,
|
|
1169
|
+
target: CommentedMap,
|
|
1170
|
+
) -> CommentedMap:
|
|
1171
|
+
|
|
1172
|
+
stack: list[tuple[Any, Any | None, Any | None]] = [(root, None, None)]
|
|
1173
|
+
|
|
1174
|
+
while stack:
|
|
1175
|
+
node, parent, accessor = stack.pop()
|
|
1176
|
+
|
|
1177
|
+
if isinstance(node, CommentedMap):
|
|
1178
|
+
for k in reversed(list(node.keys())):
|
|
1179
|
+
child = node[k]
|
|
1180
|
+
if child is target and isinstance(child, CommentedMap):
|
|
1181
|
+
return node, k
|
|
1182
|
+
|
|
1183
|
+
stack.append((child, node, k))
|
|
1184
|
+
|
|
1185
|
+
elif isinstance(node, CommentedSeq):
|
|
1186
|
+
for idx in reversed(range(len(node))):
|
|
1187
|
+
child = node[idx]
|
|
1188
|
+
if child is target and isinstance(child, CommentedMap):
|
|
1189
|
+
return node, node.index(child)
|
|
1190
|
+
|
|
1191
|
+
stack.append((child, node, idx))
|
|
1192
|
+
|
|
1193
|
+
return None, None
|
|
1194
|
+
|
|
1195
|
+
def _assemble_parameters(self, resources: YamlObject):
|
|
1196
|
+
params: dict[str, Data] = resources.get("Parameters", {})
|
|
1197
|
+
for param_name, param in params.items():
|
|
1198
|
+
if isinstance(param, CommentedMap) and (
|
|
1199
|
+
default := param.get("Default")
|
|
1200
|
+
):
|
|
1201
|
+
self._parameters_with_defaults[param_name] = default
|
|
1202
|
+
|
|
1203
|
+
def _assemble_mappings(self, mappings: dict[str, str]):
|
|
1204
|
+
for mapping, value in mappings.items():
|
|
1205
|
+
if (
|
|
1206
|
+
map_data := self._mappings.get(mapping)
|
|
1207
|
+
) and (
|
|
1208
|
+
selected := map_data.get(value)
|
|
1209
|
+
):
|
|
1210
|
+
self._selected_mappings[mapping] = selected
|
|
1211
|
+
|
|
1212
|
+
def _process_attributes(
|
|
1213
|
+
self,
|
|
1214
|
+
attributes: dict[str, Any],
|
|
1215
|
+
):
|
|
1216
|
+
return {
|
|
1217
|
+
key: self._process_python_structure(value)
|
|
1218
|
+
for key, value in attributes.items()
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
def _process_python_structure(
|
|
1222
|
+
self,
|
|
1223
|
+
obj: Any
|
|
1224
|
+
) -> Any:
|
|
1225
|
+
"""
|
|
1226
|
+
Convert arbitrarily nested Python data (dict/list/scalars) into ruamel.yaml
|
|
1227
|
+
CommentedMap/CommentedSeq equivalents using iterative DFS. Scalars are returned as-is.
|
|
1228
|
+
"""
|
|
1229
|
+
# Fast path for scalars
|
|
1230
|
+
if not isinstance(obj, (dict, list)):
|
|
1231
|
+
return obj
|
|
1232
|
+
|
|
1233
|
+
# Create root container
|
|
1234
|
+
if isinstance(obj, dict):
|
|
1235
|
+
root_out: Any = CommentedMap()
|
|
1236
|
+
work: list[tuple[Any, CommentedMap | CommentedSeq | None, Any | None]] = [(obj, None, None)]
|
|
1237
|
+
else:
|
|
1238
|
+
root_out = CommentedSeq()
|
|
1239
|
+
work = [(obj, None, None)]
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
# Map from input container id to output container to avoid recreating
|
|
1244
|
+
created: dict[int, CommentedMap | CommentedSeq] = {id(obj): root_out}
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
while work:
|
|
1248
|
+
in_node, out_parent, out_key = work.pop()
|
|
1249
|
+
|
|
1250
|
+
if isinstance(in_node, dict):
|
|
1251
|
+
out_container = created.get(id(in_node))
|
|
1252
|
+
if out_container is None:
|
|
1253
|
+
out_container = CommentedMap()
|
|
1254
|
+
created[id(in_node)] = out_container
|
|
1255
|
+
assign(out_parent, out_key, out_container)
|
|
1256
|
+
else:
|
|
1257
|
+
# Root case: already created and assigned
|
|
1258
|
+
assign(out_parent, out_key, out_container)
|
|
1259
|
+
|
|
1260
|
+
# Push children in reverse to process first child next (DFS)
|
|
1261
|
+
items = list(in_node.items())
|
|
1262
|
+
for k, v in reversed(items):
|
|
1263
|
+
if isinstance(v, (dict, list)):
|
|
1264
|
+
# Create child container placeholder now for correct parent linkage
|
|
1265
|
+
child_container = CommentedMap() if isinstance(v, dict) else CommentedSeq()
|
|
1266
|
+
created[id(v)] = child_container
|
|
1267
|
+
work.append((v, out_container, k))
|
|
1268
|
+
else:
|
|
1269
|
+
# Scalar, assign directly
|
|
1270
|
+
out_container[k] = v
|
|
1271
|
+
|
|
1272
|
+
elif isinstance(in_node, list):
|
|
1273
|
+
out_container = created.get(id(in_node))
|
|
1274
|
+
if out_container is None:
|
|
1275
|
+
out_container = CommentedSeq()
|
|
1276
|
+
created[id(in_node)] = out_container
|
|
1277
|
+
assign(out_parent, out_key, out_container)
|
|
1278
|
+
else:
|
|
1279
|
+
assign(out_parent, out_key, out_container)
|
|
1280
|
+
|
|
1281
|
+
# Push children in reverse order
|
|
1282
|
+
for idx in reversed(range(len(in_node))):
|
|
1283
|
+
v = in_node[idx]
|
|
1284
|
+
if isinstance(v, (dict, list)):
|
|
1285
|
+
child_container = CommentedMap() if isinstance(v, dict) else CommentedSeq()
|
|
1286
|
+
created[id(v)] = child_container
|
|
1287
|
+
work.append((v, out_container, idx))
|
|
1288
|
+
else:
|
|
1289
|
+
out_container.append(v)
|
|
1290
|
+
|
|
1291
|
+
else:
|
|
1292
|
+
# Scalar node
|
|
1293
|
+
assign(out_parent, out_key, in_node)
|
|
1294
|
+
|
|
1295
|
+
return root_out
|
|
1296
|
+
|
|
1297
|
+
def _resolve_template_string(self, template: str):
|
|
1298
|
+
variables: list[tuple[str, str]] = []
|
|
1299
|
+
for match in self._sub_pattern.finditer(template):
|
|
1300
|
+
variables.append((
|
|
1301
|
+
match.group(0),
|
|
1302
|
+
self._sub_inner_text_pattern.sub('', match.group(0)),
|
|
1303
|
+
))
|
|
1304
|
+
|
|
1305
|
+
return variables
|
|
1306
|
+
|
|
1307
|
+
def _resolve_sub_ref_queries(
|
|
1308
|
+
self,
|
|
1309
|
+
variables: list[tuple[str, str]],
|
|
1310
|
+
source_string: str,
|
|
1311
|
+
):
|
|
1312
|
+
for variable, accessor in variables:
|
|
1313
|
+
if val := self._references.get(accessor):
|
|
1314
|
+
source_string = source_string.replace(variable, val)
|
|
1315
|
+
|
|
1316
|
+
return source_string
|