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.
@@ -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
- functions.ALL_FUNCTIONS,
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
- functions.ALL_FUNCTIONS,
276
+ allowed_functions,
232
277
  )
233
278
 
234
279
  return template
@@ -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
- if map_name not in maps:
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
- if top_key not in map:
328
- raise KeyError(f"Unable to find key {top_key} in map {map_name}.")
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
- first_level = map[top_key]
369
+ Args:
370
+ template (Template): The template being tested.
371
+ values (Any): The values passed to the function.
331
372
 
332
- if second_key not in first_level:
333
- raise KeyError(f"Unable to find key {second_key} in map {map_name}.")
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
- return first_level[second_key]
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-radar
3
- Version: 0.10.1a0
3
+ Version: 0.11.0
4
4
  Summary: Run functional tests on cloudformation stacks.
5
5
  Home-page: https://github.com/DontShaveTheYak/cloud-radar
6
6
  License: Apache-2.0
@@ -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=kBl4lXHszS-xzzDvkVnreTQjzq84CIWWwZEWSzKS23s,29715
12
- cloud_radar/cf/unit/functions.py,sha256=YLWPhtUXJUlzQjDbx3TsN9ze-2JeWt_hKq3UCfKz8jk,25326
13
- cloud_radar-0.10.1a0.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
14
- cloud_radar-0.10.1a0.dist-info/METADATA,sha256=dr4lurpcJ7H6cS9y0zYsPqnEEU9NPz2UbgZPZC6DvGg,15366
15
- cloud_radar-0.10.1a0.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
16
- cloud_radar-0.10.1a0.dist-info/RECORD,,
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,,