cloud-radar 0.11.1a4__py3-none-any.whl → 0.12.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.
- cloud_radar/cf/unit/__init__.py +3 -1
- cloud_radar/cf/unit/_hooks.py +199 -0
- cloud_radar/cf/unit/_template.py +24 -6
- cloud_radar/cf/unit/functions.py +11 -7
- {cloud_radar-0.11.1a4.dist-info → cloud_radar-0.12.1.dist-info}/METADATA +4 -2
- {cloud_radar-0.11.1a4.dist-info → cloud_radar-0.12.1.dist-info}/RECORD +8 -7
- {cloud_radar-0.11.1a4.dist-info → cloud_radar-0.12.1.dist-info}/LICENSE.txt +0 -0
- {cloud_radar-0.11.1a4.dist-info → cloud_radar-0.12.1.dist-info}/WHEEL +0 -0
cloud_radar/cf/unit/__init__.py
CHANGED
@@ -0,0 +1,199 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
# Work around some circular import issue until someone smarter
|
4
|
+
# can work out the right way to restructure / refactor this
|
5
|
+
# Solution from https://stackoverflow.com/a/39757388/230449
|
6
|
+
from typing import TYPE_CHECKING, Callable, Dict, List, Union
|
7
|
+
|
8
|
+
from ._resource import Resource
|
9
|
+
from ._stack import Stack
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from ._template import Template
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class ResourceHookContext:
|
17
|
+
"""Class that contains the context for a resource hook to evaluate.
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
logical_id (str): The logical ID for the resource in the CloudFormation
|
21
|
+
template which is to be evaluated.
|
22
|
+
resource_definition (Resource): The definition of the resource to be evaluated.
|
23
|
+
stack (Stack): the rendered stack that the resource is part of
|
24
|
+
template (Template): the template that is being rendered to produce the stack
|
25
|
+
"""
|
26
|
+
|
27
|
+
logical_id: str
|
28
|
+
resource_definition: Resource
|
29
|
+
stack: Stack
|
30
|
+
template: "Template"
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class ResourceHookCollection:
|
35
|
+
"""
|
36
|
+
Class that holds the two collections of hooks that we can evaluate against
|
37
|
+
individual Resource items in a rendered stack.
|
38
|
+
|
39
|
+
Each Callable is expected to take in a single parameter - an instance of
|
40
|
+
ResourceHookContext.
|
41
|
+
|
42
|
+
Attributes:
|
43
|
+
plugin (Dict[str, List[Callable]]): The dict of Resource Type to
|
44
|
+
list of hooks which were loaded from plugins.
|
45
|
+
local (Dict[str, List[Callable]]): The dict of Resource Type to
|
46
|
+
list of hooks which were defined with the template.
|
47
|
+
"""
|
48
|
+
|
49
|
+
plugin: Dict[str, List[Callable]]
|
50
|
+
local: Dict[str, List[Callable]]
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class TemplateHookCollection:
|
55
|
+
"""
|
56
|
+
Class that holds the two collections of hooks that we can evaluate
|
57
|
+
against a loaded Template.
|
58
|
+
|
59
|
+
Each Callable is expected to take in a single parameter - an
|
60
|
+
instance of Template.
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
plugin (List[Callable]): The list of hooks which were loaded from plugins.
|
64
|
+
local (List[Callable]): The list of hooks which were defined with the template.
|
65
|
+
"""
|
66
|
+
|
67
|
+
plugin: List[Callable]
|
68
|
+
local: List[Callable]
|
69
|
+
|
70
|
+
|
71
|
+
class HookProcessor:
|
72
|
+
"""
|
73
|
+
Class that handles holding and evaluating the collections of hooks that we run
|
74
|
+
against Templates and Resources.
|
75
|
+
|
76
|
+
In future this will include loading hooks from plugins, but that has not been
|
77
|
+
implemented yet.
|
78
|
+
|
79
|
+
This supports suppressing rules based on Metadata at either the Template or
|
80
|
+
Resource level, using something like this:
|
81
|
+
|
82
|
+
Metadata:
|
83
|
+
Cloud-Radar:
|
84
|
+
ignore-hooks:
|
85
|
+
- s3_check_bucket_name_region
|
86
|
+
|
87
|
+
To allow this to work, you should ensure that the function names that you implement
|
88
|
+
for hooks have unique and descriptive names.
|
89
|
+
|
90
|
+
"""
|
91
|
+
|
92
|
+
def __init__(self) -> None:
|
93
|
+
self._resources: ResourceHookCollection = ResourceHookCollection(
|
94
|
+
plugin={}, local={}
|
95
|
+
)
|
96
|
+
self._template: TemplateHookCollection = TemplateHookCollection(
|
97
|
+
plugin=[], local=[]
|
98
|
+
)
|
99
|
+
# TODO: Add support for loading plugin hooks
|
100
|
+
|
101
|
+
@property
|
102
|
+
def template(self):
|
103
|
+
return self._template.local
|
104
|
+
|
105
|
+
@template.setter
|
106
|
+
def template(self, value: List[Callable]):
|
107
|
+
self._template.local = value
|
108
|
+
|
109
|
+
@property
|
110
|
+
def resources(self):
|
111
|
+
return self._resources.local
|
112
|
+
|
113
|
+
@resources.setter
|
114
|
+
def resources(self, value: Dict[str, List[Callable]]):
|
115
|
+
self._resources.local = value
|
116
|
+
|
117
|
+
def _is_hook_suppressed_in_dict(self, hook_name: str, metadata: Dict) -> bool:
|
118
|
+
cloud_radar_metadata = metadata.get("Cloud-Radar", {})
|
119
|
+
ignored_hooks = cloud_radar_metadata.get("ignore-hooks", {})
|
120
|
+
|
121
|
+
return hook_name in ignored_hooks
|
122
|
+
|
123
|
+
def _is_hook_suppressed(
|
124
|
+
self, hook_name, template: "Template", resource: Union[Resource, None] = None
|
125
|
+
) -> bool:
|
126
|
+
# First check if a suppression exists in the template,
|
127
|
+
# as that will take precedence.
|
128
|
+
hook_suppressed = self._is_hook_suppressed_in_dict(
|
129
|
+
hook_name, template.template.get("Metadata", {})
|
130
|
+
)
|
131
|
+
|
132
|
+
if not hook_suppressed and resource:
|
133
|
+
# if not suppression was found at the template level, check the
|
134
|
+
# resource level
|
135
|
+
hook_suppressed = self._is_hook_suppressed_in_dict(
|
136
|
+
hook_name, resource.get("Metadata", {})
|
137
|
+
)
|
138
|
+
|
139
|
+
return hook_suppressed
|
140
|
+
|
141
|
+
def _evaluate_template_hooks(
|
142
|
+
self, hook_type: str, hooks: List[Callable], template: "Template"
|
143
|
+
) -> None:
|
144
|
+
for single_hook in hooks:
|
145
|
+
# Only process the hook if it has not been marked as to be
|
146
|
+
# ignored
|
147
|
+
if not self._is_hook_suppressed(single_hook.__name__, template, None):
|
148
|
+
print(f"Processing {hook_type} hook {single_hook.__name__}")
|
149
|
+
|
150
|
+
single_hook(template=template)
|
151
|
+
|
152
|
+
def _evaluate_resource_hooks(
|
153
|
+
self,
|
154
|
+
hook_type: str,
|
155
|
+
hooks: Dict[str, List[Callable]],
|
156
|
+
stack: Stack,
|
157
|
+
template: "Template",
|
158
|
+
) -> None:
|
159
|
+
# Iterate through the resources in the rendered stack
|
160
|
+
for logical_id in stack.data.get("Resources", {}):
|
161
|
+
print("Got resource " + logical_id)
|
162
|
+
resource_definition = stack.get_resource(logical_id)
|
163
|
+
|
164
|
+
resource_type = resource_definition.get("Type")
|
165
|
+
|
166
|
+
# Get the hooks that have been defined for this type of resource
|
167
|
+
type_hooks = hooks.get(resource_type, [])
|
168
|
+
|
169
|
+
hook_context = ResourceHookContext(
|
170
|
+
logical_id=logical_id,
|
171
|
+
resource_definition=resource_definition,
|
172
|
+
stack=stack,
|
173
|
+
template=template,
|
174
|
+
)
|
175
|
+
|
176
|
+
# Iterate through each defined hook and call them.
|
177
|
+
for single_hook in type_hooks:
|
178
|
+
# Only process the hook if it has not been marked as to be
|
179
|
+
# ignored
|
180
|
+
if not self._is_hook_suppressed(
|
181
|
+
single_hook.__name__, template, resource_definition
|
182
|
+
):
|
183
|
+
print(f"Processing {hook_type} hook {single_hook.__name__}")
|
184
|
+
|
185
|
+
single_hook(context=hook_context)
|
186
|
+
|
187
|
+
def evaluate_resource_hooks(self, stack: Stack, template: "Template") -> None:
|
188
|
+
# Evaluate the global hooks first, then the local ones
|
189
|
+
self._evaluate_resource_hooks("plugin", self._resources.plugin, stack, template)
|
190
|
+
self._evaluate_resource_hooks("local", self._resources.local, stack, template)
|
191
|
+
|
192
|
+
def evaluate_template_hooks(self, template: "Template") -> None:
|
193
|
+
print(template)
|
194
|
+
# raise ValueError(type(template))
|
195
|
+
# raise ValueError(template.template)
|
196
|
+
|
197
|
+
# Evaluate the global hooks first, then the local ones
|
198
|
+
self._evaluate_template_hooks("plugin", self._template.plugin, template)
|
199
|
+
self._evaluate_template_hooks("local", self._template.local, template)
|
cloud_radar/cf/unit/_template.py
CHANGED
@@ -9,6 +9,7 @@ import yaml # noqa: I100
|
|
9
9
|
from cfn_tools import dump_yaml, load_yaml # type: ignore # noqa: I100, I201
|
10
10
|
|
11
11
|
from . import functions
|
12
|
+
from ._hooks import HookProcessor
|
12
13
|
from ._stack import Stack
|
13
14
|
|
14
15
|
IntrinsicFunc = Callable[["Template", Any], Any]
|
@@ -20,6 +21,7 @@ class Template:
|
|
20
21
|
"""
|
21
22
|
|
22
23
|
AccountId: str = "5" * 12
|
24
|
+
Hooks = HookProcessor()
|
23
25
|
NotificationARNs: list = []
|
24
26
|
NoValue: str = "" # Not yet implemented
|
25
27
|
Partition: str = "aws" # Other regions not implemented
|
@@ -79,6 +81,10 @@ class Template:
|
|
79
81
|
"Transform", None
|
80
82
|
)
|
81
83
|
|
84
|
+
# All loaded, validate against any template level hooks
|
85
|
+
# that have been configured
|
86
|
+
self.Hooks.evaluate_template_hooks(self)
|
87
|
+
|
82
88
|
@classmethod
|
83
89
|
def from_yaml(
|
84
90
|
cls,
|
@@ -317,6 +323,9 @@ class Template:
|
|
317
323
|
|
318
324
|
stack = Stack(self.template)
|
319
325
|
|
326
|
+
# Evaluate any hooks prior to returning this stack
|
327
|
+
self.Hooks.evaluate_resource_hooks(stack, self)
|
328
|
+
|
320
329
|
return stack
|
321
330
|
|
322
331
|
def resolve_values( # noqa: max-complexity: 13
|
@@ -596,7 +605,12 @@ def validate_aws_parameter_constraints(
|
|
596
605
|
|
597
606
|
# There are a few variants of SSM parameters, but they all have the
|
598
607
|
# same regex pattern
|
599
|
-
|
608
|
+
#
|
609
|
+
# This is based on the documentation for the PutParameter API operation
|
610
|
+
# https://docs.aws.amazon.com/systems-manager/latest/APIReference/
|
611
|
+
# API_PutParameter.html#systemsmanager-PutParameter-request-Name
|
612
|
+
#
|
613
|
+
ssm_parameter_value_regex = r"^([/]{0,1}[a-zA-Z0-9_.-]*){1,15}$"
|
600
614
|
|
601
615
|
if parameter_type.startswith("AWS::SSM::Parameter::Value<"):
|
602
616
|
# SSM parameter, need to validate that the type in the angle brackets
|
@@ -640,7 +654,7 @@ def validate_aws_parameter_constraints(
|
|
640
654
|
raise ValueError(
|
641
655
|
(
|
642
656
|
f"Value {parameter_value} does not match the expected pattern "
|
643
|
-
f"for SSM parameter {parameter_name}"
|
657
|
+
f"for SSM parameter {parameter_name}."
|
644
658
|
)
|
645
659
|
)
|
646
660
|
|
@@ -784,7 +798,7 @@ def validate_string_parameter_constraints(
|
|
784
798
|
|
785
799
|
def add_metadata(template: Dict, region: str) -> None:
|
786
800
|
"""This functions adds the current region to the template
|
787
|
-
as
|
801
|
+
as metadata because we can't treat Region like a normal pseudo
|
788
802
|
variables because we don't want to update the class var for every run.
|
789
803
|
|
790
804
|
Args:
|
@@ -792,12 +806,16 @@ def add_metadata(template: Dict, region: str) -> None:
|
|
792
806
|
region (str): The region that template will be tested with.
|
793
807
|
"""
|
794
808
|
|
795
|
-
metadata = {"Cloud-Radar": {"Region": region}}
|
796
|
-
|
797
809
|
if "Metadata" not in template:
|
798
810
|
template["Metadata"] = {}
|
799
811
|
|
800
|
-
|
812
|
+
# Get the existing metadata (so we do not overwrite any
|
813
|
+
# hook suppressions), then set the region into it before
|
814
|
+
# updating the template
|
815
|
+
cloud_radar_metadata = template["Metadata"].get("Cloud-Radar", {})
|
816
|
+
cloud_radar_metadata["Region"] = region
|
817
|
+
|
818
|
+
template["Metadata"]["Cloud-Radar"] = cloud_radar_metadata
|
801
819
|
|
802
820
|
|
803
821
|
# All the other Cloudformation intrinsic functions start with `Fn:` but for some reason
|
cloud_radar/cf/unit/functions.py
CHANGED
@@ -830,17 +830,21 @@ def ref(template: "Template", var_name: str) -> Any:
|
|
830
830
|
|
831
831
|
if "Parameters" in template.template:
|
832
832
|
if var_name in template.template["Parameters"]:
|
833
|
+
# This is a reference to a parameter
|
834
|
+
|
833
835
|
param_def = template.template["Parameters"][var_name]
|
834
|
-
|
835
|
-
|
836
|
-
|
836
|
+
param_type = param_def.get("Type", "")
|
837
|
+
param_value = param_def["Value"]
|
838
|
+
|
839
|
+
if param_type.startswith("AWS::SSM::Parameter::Value<"):
|
837
840
|
# This is an SSM parameter value, look it up from our dynamic references
|
838
|
-
return template._get_dynamic_reference_value(
|
839
|
-
|
840
|
-
|
841
|
+
return template._get_dynamic_reference_value("ssm", param_value)
|
842
|
+
if param_type == "CommaDelimitedList" or param_type.startswith("List<"):
|
843
|
+
# Return the value split into a list of strings
|
844
|
+
return param_value.split(",")
|
841
845
|
|
842
846
|
# If we get this far, regular parameter value to lookup & return
|
843
|
-
return
|
847
|
+
return param_value
|
844
848
|
|
845
849
|
if var_name in template.template["Resources"]:
|
846
850
|
return var_name
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: cloud-radar
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.12.1
|
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
|
@@ -114,6 +114,8 @@ You can Test:
|
|
114
114
|
|
115
115
|
You can test all this locally without worrying about AWS Credentials.
|
116
116
|
|
117
|
+
A number of these tests can be configured in a common way to apply to all templates through the use of the [hooks](./examples/unit/hooks/README.md) functionality.
|
118
|
+
|
117
119
|
### Functional Testing
|
118
120
|
|
119
121
|
This project is a wrapper around Taskcat. Taskcat is a great tool for ensuring your Cloudformation Template can be deployed in multiple AWS Regions. Cloud-Radar enhances Taskcat by making it easier to write more complete functional tests.
|
@@ -197,7 +199,7 @@ The default values for pseudo parameters:
|
|
197
199
|
| **StackId** | "" |
|
198
200
|
| **StackName** | "" |
|
199
201
|
| **URLSuffix** | "amazonaws.com" |
|
200
|
-
_Note: Bold variables are not fully
|
202
|
+
_Note: Bold variables are not fully implemented yet see the [Roadmap](#roadmap)_
|
201
203
|
|
202
204
|
At the point of creating the `Template` instance additional configuration is required to be provided if you are using certain approaches to resolving values.
|
203
205
|
|
@@ -2,16 +2,17 @@ cloud_radar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
cloud_radar/cf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
cloud_radar/cf/e2e/__init__.py,sha256=83Bf_ftCxMimG1mxQH0xsKFCmsrQ7YJEAQkeZtJIxL0,47
|
4
4
|
cloud_radar/cf/e2e/_stack.py,sha256=Qm5vQsUSUluHirTFTs_n40yI6Z8-HMZ837OvSJcyWqM,1676
|
5
|
-
cloud_radar/cf/unit/__init__.py,sha256=
|
5
|
+
cloud_radar/cf/unit/__init__.py,sha256=H9iQw4PSZEW-u0Ey-Z8eZfzQ6GJ7wdxmkKE8Z09U7pU,198
|
6
6
|
cloud_radar/cf/unit/_condition.py,sha256=e7gwcz87yF6dKl2quxmOmjzHku8OW6EWAsgbCeDMARg,711
|
7
|
+
cloud_radar/cf/unit/_hooks.py,sha256=3_uMDzIs6rt_TZfP5-Qz5gIBmJo8rxd2GBBXxTVZsa0,6914
|
7
8
|
cloud_radar/cf/unit/_output.py,sha256=rhQQ4aJu06bxAoG7GOXkuOPzAmLCrTA2Ytb8rNXA4Vg,1822
|
8
9
|
cloud_radar/cf/unit/_parameter.py,sha256=AdPIqc2ggSW1VR0USF30zjphX3z1HHbGKQt8n-Kzhfo,3199
|
9
10
|
cloud_radar/cf/unit/_resource.py,sha256=dWaR-5s6ea5mIu6Dhf1hY31Wd4WLHbHsbyxnu2Tz6QI,8512
|
10
11
|
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=
|
12
|
+
cloud_radar/cf/unit/_template.py,sha256=u1r1RkUPI0t8SGolWEjb3doavFwUQRaE0wOKCi_ijiI,32242
|
13
|
+
cloud_radar/cf/unit/functions.py,sha256=k1i12FDChgLwGsG-3q9kdYbuEOgnMEs0qPynp-YcZAM,28494
|
13
14
|
cloud_radar/cf/unit/test__template.py,sha256=jVPMJTn6Q0sSZ8BjRGyutuR9-NjdHdwDTVsd2kvjQbs,1491
|
14
|
-
cloud_radar-0.
|
15
|
-
cloud_radar-0.
|
16
|
-
cloud_radar-0.
|
17
|
-
cloud_radar-0.
|
15
|
+
cloud_radar-0.12.1.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
16
|
+
cloud_radar-0.12.1.dist-info/METADATA,sha256=vwvpMBogg8baN7ninA1rMU_jFP7lpotb_xbhkYY9tu8,15527
|
17
|
+
cloud_radar-0.12.1.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
18
|
+
cloud_radar-0.12.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|