cloud-radar 0.10.1a0__py3-none-any.whl → 0.11.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.
- cloud_radar/cf/unit/_template.py +48 -3
- cloud_radar/cf/unit/functions.py +99 -9
- cloud_radar/cf/unit/test__template.py +46 -0
- {cloud_radar-0.10.1a0.dist-info → cloud_radar-0.11.0.dist-info}/METADATA +1 -1
- {cloud_radar-0.10.1a0.dist-info → cloud_radar-0.11.0.dist-info}/RECORD +7 -6
- {cloud_radar-0.10.1a0.dist-info → cloud_radar-0.11.0.dist-info}/LICENSE.txt +0 -0
- {cloud_radar-0.10.1a0.dist-info → cloud_radar-0.11.0.dist-info}/WHEEL +0 -0
cloud_radar/cf/unit/_template.py
CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import json
|
4
4
|
import re
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Any, Callable, Dict, Generator, Optional, Tuple, Union
|
6
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
|
7
7
|
|
8
8
|
import yaml # noqa: I100
|
9
9
|
from cfn_tools import dump_yaml, load_yaml # type: ignore # noqa: I100, I201
|
@@ -75,6 +75,9 @@ class Template:
|
|
75
75
|
self.Region = Template.Region
|
76
76
|
self.imports = imports
|
77
77
|
self.dynamic_references = dynamic_references
|
78
|
+
self.transforms: Optional[Union[str, List[str]]] = self.template.get(
|
79
|
+
"Transform", None
|
80
|
+
)
|
78
81
|
|
79
82
|
@classmethod
|
80
83
|
def from_yaml(
|
@@ -197,12 +200,54 @@ class Template:
|
|
197
200
|
# If we get this far then we do not support this type of configuration file
|
198
201
|
raise ValueError("Parameter file is not in a supported format")
|
199
202
|
|
203
|
+
def load_allowed_functions(self) -> functions.Dispatch:
|
204
|
+
"""Loads the allowed functions for this template.
|
205
|
+
|
206
|
+
Raises:
|
207
|
+
ValueError: If the transform is not supported.
|
208
|
+
ValueError: If the Transform section is not a string or list.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
functions.Dispatch: A dictionary of allowed functions.
|
212
|
+
"""
|
213
|
+
if self.transforms is None:
|
214
|
+
return functions.ALL_FUNCTIONS
|
215
|
+
|
216
|
+
if isinstance(self.transforms, str):
|
217
|
+
if self.transforms not in functions.TRANSFORMS:
|
218
|
+
raise ValueError(f"Transform {self.transforms} not supported")
|
219
|
+
|
220
|
+
# dict of transform functions
|
221
|
+
transform_functions = functions.TRANSFORMS[self.transforms]
|
222
|
+
|
223
|
+
# return the merger of ALL_FUNCTIONS and the transform functions
|
224
|
+
return {**functions.ALL_FUNCTIONS, **transform_functions}
|
225
|
+
|
226
|
+
if isinstance(self.transforms, list):
|
227
|
+
# dict of transform functions
|
228
|
+
transform_functions = {}
|
229
|
+
for transform in self.transforms:
|
230
|
+
if transform not in functions.TRANSFORMS:
|
231
|
+
raise ValueError(f"Transform {transform} not supported")
|
232
|
+
transform_functions = {
|
233
|
+
**transform_functions,
|
234
|
+
**functions.TRANSFORMS[transform],
|
235
|
+
}
|
236
|
+
|
237
|
+
# return the merger of ALL_FUNCTIONS and the transform functions
|
238
|
+
return {**functions.ALL_FUNCTIONS, **transform_functions}
|
239
|
+
|
240
|
+
raise ValueError(f"Transform {self.transforms} not supported")
|
241
|
+
|
200
242
|
def render_all_sections(self, template: Dict[str, Any]) -> Dict[str, Any]:
|
201
243
|
"""Solves all conditionals, references and pseudo variables for all sections"""
|
244
|
+
|
245
|
+
allowed_functions = self.load_allowed_functions()
|
246
|
+
|
202
247
|
if "Conditions" in template:
|
203
248
|
template["Conditions"] = self.resolve_values(
|
204
249
|
template["Conditions"],
|
205
|
-
|
250
|
+
allowed_functions,
|
206
251
|
)
|
207
252
|
|
208
253
|
template_sections = ["Resources", "Outputs"]
|
@@ -228,7 +273,7 @@ class Template:
|
|
228
273
|
|
229
274
|
template[section][r_name] = self.resolve_values(
|
230
275
|
r_value,
|
231
|
-
|
276
|
+
allowed_functions,
|
232
277
|
)
|
233
278
|
|
234
279
|
return template
|
cloud_radar/cf/unit/functions.py
CHANGED
@@ -15,8 +15,16 @@ import requests
|
|
15
15
|
if TYPE_CHECKING:
|
16
16
|
from ._template import Template
|
17
17
|
|
18
|
+
# Dispatch represents a dictionary where the keys are Cloudformation
|
19
|
+
# function names in there long form and the values are the functions
|
20
|
+
# in python that solve them.
|
18
21
|
Dispatch = Dict[str, Callable[..., Any]]
|
19
22
|
|
23
|
+
# Mapping represents the Cloudformation Mappings section of a template.
|
24
|
+
# The keys are the names of the maps and the values are the maps themselves.
|
25
|
+
# The maps are a nested dictionary.
|
26
|
+
Mapping = Dict[str, Dict[str, Dict[str, Any]]]
|
27
|
+
|
20
28
|
REGION_DATA = None
|
21
29
|
|
22
30
|
|
@@ -281,6 +289,39 @@ def condition(template: "Template", name: Any) -> bool:
|
|
281
289
|
return condition_value
|
282
290
|
|
283
291
|
|
292
|
+
def _find_in_map(maps: Mapping, map_name: str, top_key: str, second_key: str) -> Any:
|
293
|
+
"""Solves AWS FindInMap intrinsic function.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
maps (Mapping): The Cloudformation Mappings section of the template.
|
297
|
+
map_name (str): The name of the Map to search.
|
298
|
+
top_key (str): The top level key to search.
|
299
|
+
second_key (str): The second level key to search.
|
300
|
+
|
301
|
+
Raises:
|
302
|
+
KeyError: If map_name is not found in the Mapping section.
|
303
|
+
KeyError: If top_key is not found in the Map.
|
304
|
+
KeyError: If second_key is not found in the Map.
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Any: The requested value from the Map.
|
308
|
+
"""
|
309
|
+
if map_name not in maps:
|
310
|
+
raise KeyError(f"Unable to find {map_name} in Mappings section of template.")
|
311
|
+
|
312
|
+
map = maps[map_name]
|
313
|
+
|
314
|
+
if top_key not in map:
|
315
|
+
raise KeyError(f"Unable to find key {top_key} in map {map_name}.")
|
316
|
+
|
317
|
+
first_level = map[top_key]
|
318
|
+
|
319
|
+
if second_key not in first_level:
|
320
|
+
raise KeyError(f"Unable to find key {second_key} in map {map_name}.")
|
321
|
+
|
322
|
+
return first_level[second_key]
|
323
|
+
|
324
|
+
|
284
325
|
def find_in_map(template: "Template", values: Any) -> Any:
|
285
326
|
"""Solves AWS FindInMap intrinsic function.
|
286
327
|
|
@@ -319,20 +360,57 @@ def find_in_map(template: "Template", values: Any) -> Any:
|
|
319
360
|
|
320
361
|
maps = template.template["Mappings"]
|
321
362
|
|
322
|
-
|
323
|
-
raise KeyError(f"Unable to find {map_name} in Mappings section of template.")
|
363
|
+
return _find_in_map(maps, map_name, top_key, second_key)
|
324
364
|
|
325
|
-
map = maps[map_name]
|
326
365
|
|
327
|
-
|
328
|
-
|
366
|
+
def enhanced_find_in_map(template: "Template", values: Any) -> Any:
|
367
|
+
"""Solves AWS FindInMap intrinsic function. This version allows for a default value.
|
329
368
|
|
330
|
-
|
369
|
+
Args:
|
370
|
+
template (Template): The template being tested.
|
371
|
+
values (Any): The values passed to the function.
|
331
372
|
|
332
|
-
|
333
|
-
|
373
|
+
Raises:
|
374
|
+
TypeError: If values is not a list.
|
375
|
+
ValueError: If length of values is not 3.
|
376
|
+
KeyError: If the Map or specified keys are missing.
|
334
377
|
|
335
|
-
|
378
|
+
Returns:
|
379
|
+
Any: The requested value from the Map.
|
380
|
+
"""
|
381
|
+
|
382
|
+
if not isinstance(values, list):
|
383
|
+
raise TypeError(
|
384
|
+
f"Fn::FindInMap - The values must be a List, not {type(values).__name__}."
|
385
|
+
)
|
386
|
+
|
387
|
+
if len(values) not in [3, 4]:
|
388
|
+
raise ValueError(
|
389
|
+
(
|
390
|
+
"Fn::FindInMap - The values must contain "
|
391
|
+
"a MapName, TopLevelKey and SecondLevelKey. "
|
392
|
+
"Optionally, a third value can be provided to "
|
393
|
+
"specify a default value."
|
394
|
+
)
|
395
|
+
)
|
396
|
+
|
397
|
+
map_name = values[0]
|
398
|
+
top_key = values[1]
|
399
|
+
second_key = values[2]
|
400
|
+
|
401
|
+
if "Mappings" not in template.template:
|
402
|
+
raise KeyError("Unable to find Mappings section in template.")
|
403
|
+
|
404
|
+
maps = template.template["Mappings"]
|
405
|
+
|
406
|
+
default_value: Dict[str, Any] = values.pop(3) if len(values) == 4 else {}
|
407
|
+
|
408
|
+
try:
|
409
|
+
return _find_in_map(maps, map_name, top_key, second_key)
|
410
|
+
except KeyError:
|
411
|
+
if "DefaultValue" in default_value:
|
412
|
+
return default_value["DefaultValue"]
|
413
|
+
raise
|
336
414
|
|
337
415
|
|
338
416
|
def get_att(template: "Template", values: Any) -> str:
|
@@ -941,3 +1019,15 @@ ALLOWED_FUNCTIONS: Dict[str, Dispatch] = {
|
|
941
1019
|
"Fn::Transform": {}, # Transform isn't fully implemented
|
942
1020
|
"Ref": {}, # String only.
|
943
1021
|
}
|
1022
|
+
|
1023
|
+
# Extra functions that are allowed if the template is using a transform.
|
1024
|
+
TRANSFORMS: Dict[str, Dispatch] = {
|
1025
|
+
"AWS::CodeDeployBlueGreen": {},
|
1026
|
+
"AWS::Include": {},
|
1027
|
+
"AWS::LanguageExtensions": {
|
1028
|
+
"Fn::FindInMap": enhanced_find_in_map,
|
1029
|
+
},
|
1030
|
+
"AWS::SecretsManager-2020-07-23": {},
|
1031
|
+
"AWS::Serverless-2016-10-31": {},
|
1032
|
+
"AWS::ServiceCatalog": {},
|
1033
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Just a note that GitHub Copilot generated this entire file, first try
|
2
|
+
import pytest
|
3
|
+
|
4
|
+
from cloud_radar.cf.unit import functions
|
5
|
+
from cloud_radar.cf.unit._template import Template
|
6
|
+
|
7
|
+
|
8
|
+
def test_load_allowed_functions_no_transforms():
|
9
|
+
template = Template({})
|
10
|
+
result = template.load_allowed_functions()
|
11
|
+
assert result == functions.ALL_FUNCTIONS
|
12
|
+
|
13
|
+
|
14
|
+
def test_load_allowed_functions_single_transform():
|
15
|
+
template = Template({"Transform": "AWS::Serverless-2016-10-31"})
|
16
|
+
result = template.load_allowed_functions()
|
17
|
+
expected = {
|
18
|
+
**functions.ALL_FUNCTIONS,
|
19
|
+
**functions.TRANSFORMS["AWS::Serverless-2016-10-31"],
|
20
|
+
}
|
21
|
+
assert result == expected
|
22
|
+
|
23
|
+
|
24
|
+
def test_load_allowed_functions_multiple_transforms():
|
25
|
+
template = Template({"Transform": ["AWS::Serverless-2016-10-31", "AWS::Include"]})
|
26
|
+
result = template.load_allowed_functions()
|
27
|
+
expected = {
|
28
|
+
**functions.ALL_FUNCTIONS,
|
29
|
+
**functions.TRANSFORMS["AWS::Serverless-2016-10-31"],
|
30
|
+
**functions.TRANSFORMS["AWS::Include"],
|
31
|
+
}
|
32
|
+
assert result == expected
|
33
|
+
|
34
|
+
|
35
|
+
def test_load_allowed_functions_invalid_transform():
|
36
|
+
template = Template({"Transform": "InvalidTransform"})
|
37
|
+
with pytest.raises(ValueError):
|
38
|
+
template.load_allowed_functions()
|
39
|
+
|
40
|
+
|
41
|
+
def test_load_allowed_functions_invalid_transforms():
|
42
|
+
template = Template(
|
43
|
+
{"Transform": ["AWS::Serverless-2016-10-31", "InvalidTransform"]}
|
44
|
+
)
|
45
|
+
with pytest.raises(ValueError):
|
46
|
+
template.load_allowed_functions()
|
@@ -8,9 +8,10 @@ cloud_radar/cf/unit/_output.py,sha256=rhQQ4aJu06bxAoG7GOXkuOPzAmLCrTA2Ytb8rNXA4V
|
|
8
8
|
cloud_radar/cf/unit/_parameter.py,sha256=AdPIqc2ggSW1VR0USF30zjphX3z1HHbGKQt8n-Kzhfo,3199
|
9
9
|
cloud_radar/cf/unit/_resource.py,sha256=dWaR-5s6ea5mIu6Dhf1hY31Wd4WLHbHsbyxnu2Tz6QI,8512
|
10
10
|
cloud_radar/cf/unit/_stack.py,sha256=_S0L9O7Lw-QAJDKubClp2b6UYtYfyzg272_7WQkUdo8,5785
|
11
|
-
cloud_radar/cf/unit/_template.py,sha256=
|
12
|
-
cloud_radar/cf/unit/functions.py,sha256=
|
13
|
-
cloud_radar
|
14
|
-
cloud_radar-0.
|
15
|
-
cloud_radar-0.
|
16
|
-
cloud_radar-0.
|
11
|
+
cloud_radar/cf/unit/_template.py,sha256=geGy8fiO6esi6a3iwwpUTEJy04WivLYzsVSBCjCO7aE,31463
|
12
|
+
cloud_radar/cf/unit/functions.py,sha256=KPPPNLrXzYMi5dtsXkvi0RvaaeAnvBC_2MhqHPs1lHo,28329
|
13
|
+
cloud_radar/cf/unit/test__template.py,sha256=jVPMJTn6Q0sSZ8BjRGyutuR9-NjdHdwDTVsd2kvjQbs,1491
|
14
|
+
cloud_radar-0.11.0.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
15
|
+
cloud_radar-0.11.0.dist-info/METADATA,sha256=H9Jy-h7ocCGl0HRe1JJS7p4Jct9N3PNKz8zOrF5Jfr4,15364
|
16
|
+
cloud_radar-0.11.0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
17
|
+
cloud_radar-0.11.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|