cloud-radar 0.13.3a16__tar.gz → 0.14.0__tar.gz
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-0.13.3a16 → cloud_radar-0.14.0}/PKG-INFO +31 -16
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/README.md +23 -5
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/pyproject.toml +29 -20
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_template.py +29 -36
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/functions.py +13 -104
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/test__template.py +10 -12
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/LICENSE.txt +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/__init__.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/__init__.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/e2e/__init__.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/e2e/_stack.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/__init__.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_condition.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_hooks.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_output.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_parameter.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_resource.py +0 -0
- {cloud_radar-0.13.3a16 → cloud_radar-0.14.0}/src/cloud_radar/cf/unit/_stack.py +0 -0
@@ -1,13 +1,12 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: cloud-radar
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.0
|
4
4
|
Summary: Run functional tests on cloudformation stacks.
|
5
|
-
Home-page: https://github.com/DontShaveTheYak/cloud-radar
|
6
5
|
License: Apache-2.0
|
7
6
|
Keywords: aws,cloudformation,cloud-radar,testing,taskcat,cloud,radar
|
8
7
|
Author: Levi Blaney
|
9
|
-
Author-email: shadycuz@gmail.com
|
10
|
-
Requires-Python: >=3.9,<
|
8
|
+
Author-email: shadycuz+dev@gmail.com
|
9
|
+
Requires-Python: >=3.9,<4.0
|
11
10
|
Classifier: Development Status :: 2 - Pre-Alpha
|
12
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
13
12
|
Classifier: Operating System :: OS Independent
|
@@ -15,17 +14,15 @@ Classifier: Programming Language :: Python :: 3
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
18
|
-
Classifier: Programming Language :: Python :: 3
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
21
17
|
Classifier: Programming Language :: Python :: 3.12
|
22
18
|
Classifier: Programming Language :: Python :: 3.13
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
24
19
|
Classifier: Topic :: Software Development :: Libraries
|
25
20
|
Classifier: Topic :: Software Development :: Testing
|
26
|
-
Requires-Dist: botocore (>=1.35.36)
|
21
|
+
Requires-Dist: botocore (>=1.35.36,<2.0.0)
|
27
22
|
Requires-Dist: cfn-flip (>=1.3.0,<2.0.0)
|
28
|
-
Requires-Dist: taskcat (>=0.9.41,<0.
|
23
|
+
Requires-Dist: taskcat (>=0.9.41,<1.0.0)
|
24
|
+
Project-URL: Changelog, https://github.com/DontShaveTheYak/cloud-radar/releases
|
25
|
+
Project-URL: Issues, https://github.com/DontShaveTheYak/cloud-radar/issues
|
29
26
|
Project-URL: Repository, https://github.com/DontShaveTheYak/cloud-radar
|
30
27
|
Description-Content-Type: text/markdown
|
31
28
|
|
@@ -37,7 +34,7 @@ Description-Content-Type: text/markdown
|
|
37
34
|
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
38
35
|
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
39
36
|
-->
|
40
|
-
[![Python][
|
37
|
+
[![Python][py-versions-shield]][pypi-url]
|
41
38
|
[![Latest][version-shield]][pypi-url]
|
42
39
|
[![Tests][test-shield]][test-url]
|
43
40
|
[![Coverage][codecov-shield]][codecov-url]
|
@@ -200,8 +197,8 @@ The default values for pseudo parameters:
|
|
200
197
|
| **NoValue** | "" |
|
201
198
|
| **Partition** | "aws" |
|
202
199
|
| Region | "us-east-1" |
|
203
|
-
|
|
204
|
-
|
|
200
|
+
| StackId | (generated based on other values) |
|
201
|
+
| StackName | "my-cloud-radar-stack" |
|
205
202
|
| **URLSuffix** | "amazonaws.com" |
|
206
203
|
_Note: Bold variables are not fully implemented yet see the [Roadmap](#roadmap)_
|
207
204
|
|
@@ -242,6 +239,24 @@ dynamic_references = {
|
|
242
239
|
template = Template(template_content, dynamic_references=dynamic_references)
|
243
240
|
```
|
244
241
|
|
242
|
+
There are cases where the default behaviour of our `GetAtt` implementation may not be sufficient and you need a more accurate returned value. When unit testing there are no real AWS resources created, and cloud-radar does not attempt to realistically generate attribute values - a string is always returned. This works good enough most of the time, but there are some cases where if you are attempting to apply intrinsic functions against the attribute value it needs to be more correct. When this occurs, you can add Metadata to the template to provide test values to use.
|
243
|
+
|
244
|
+
```
|
245
|
+
Resources:
|
246
|
+
MediaPackageV2Channel:
|
247
|
+
Type: AWS::MediaPackageV2::Channel
|
248
|
+
Metadata:
|
249
|
+
Cloud-Radar:
|
250
|
+
attribute-values:
|
251
|
+
# Default behaviour of a string is not good enough here, the attribute value is expected to be a List.
|
252
|
+
IngestEndpointUrls:
|
253
|
+
- http://one.example.com
|
254
|
+
- http://two.example.com
|
255
|
+
Properties:
|
256
|
+
ChannelGroupName: dev_video_1
|
257
|
+
ChannelName: !Sub ${AWS::StackName}-MediaPackageChannel
|
258
|
+
```
|
259
|
+
|
245
260
|
A real unit testing example using Pytest can be seen [here](./tests/test_cf/test_examples/test_unit.py)
|
246
261
|
|
247
262
|
</details>
|
@@ -338,7 +353,6 @@ A real functional testing example using Pytest can be seen [here](./tests/test_c
|
|
338
353
|
### Unit
|
339
354
|
- Add full functionality to pseudo variables.
|
340
355
|
* Variables like `Partition`, `URLSuffix` should change if the region changes.
|
341
|
-
* Variables like `StackName` and `StackId` should have a better default than ""
|
342
356
|
- Handle References to resources that shouldn't exist.
|
343
357
|
* It's currently possible that a `!Ref` to a Resource stays in the final template even if that resource is later removed because of a conditional.
|
344
358
|
|
@@ -374,11 +388,12 @@ Levi - [@shady_cuz](https://twitter.com/shady_cuz)
|
|
374
388
|
* [Taskcat](https://aws-quickstart.github.io/taskcat/)
|
375
389
|
* [Hypermodern Python](https://cjolowicz.github.io/posts/hypermodern-python-01-setup/)
|
376
390
|
* [Best-README-Template](https://github.com/othneildrew/Best-README-Template)
|
377
|
-
* @dhutchison - He was the first contributor to this project and finished the last couple of features to make this project complete. Thank you!
|
391
|
+
* [David Hutchison (@dhutchison)](https://github.com/dhutchison) - He was the first contributor to this project and finished the last couple of features to make this project complete. Thank you!
|
378
392
|
|
379
393
|
<!-- MARKDOWN LINKS & IMAGES -->
|
380
394
|
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
381
395
|
[python-shield]: https://img.shields.io/pypi/pyversions/cloud-radar?style=for-the-badge
|
396
|
+
[py-versions-shield]: https://img.shields.io/pypi/pyversions/cloud-radar?style=for-the-badge
|
382
397
|
[version-shield]: https://img.shields.io/pypi/v/cloud-radar?label=latest&style=for-the-badge
|
383
398
|
[pypi-url]: https://pypi.org/project/cloud-radar/
|
384
399
|
[test-shield]: https://img.shields.io/github/actions/workflow/status/DontShaveTheYak/cloud-radar/test.yml?label=Tests&style=for-the-badge
|
@@ -6,7 +6,7 @@
|
|
6
6
|
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
|
7
7
|
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
|
8
8
|
-->
|
9
|
-
[![Python][
|
9
|
+
[![Python][py-versions-shield]][pypi-url]
|
10
10
|
[![Latest][version-shield]][pypi-url]
|
11
11
|
[![Tests][test-shield]][test-url]
|
12
12
|
[![Coverage][codecov-shield]][codecov-url]
|
@@ -169,8 +169,8 @@ The default values for pseudo parameters:
|
|
169
169
|
| **NoValue** | "" |
|
170
170
|
| **Partition** | "aws" |
|
171
171
|
| Region | "us-east-1" |
|
172
|
-
|
|
173
|
-
|
|
172
|
+
| StackId | (generated based on other values) |
|
173
|
+
| StackName | "my-cloud-radar-stack" |
|
174
174
|
| **URLSuffix** | "amazonaws.com" |
|
175
175
|
_Note: Bold variables are not fully implemented yet see the [Roadmap](#roadmap)_
|
176
176
|
|
@@ -211,6 +211,24 @@ dynamic_references = {
|
|
211
211
|
template = Template(template_content, dynamic_references=dynamic_references)
|
212
212
|
```
|
213
213
|
|
214
|
+
There are cases where the default behaviour of our `GetAtt` implementation may not be sufficient and you need a more accurate returned value. When unit testing there are no real AWS resources created, and cloud-radar does not attempt to realistically generate attribute values - a string is always returned. This works good enough most of the time, but there are some cases where if you are attempting to apply intrinsic functions against the attribute value it needs to be more correct. When this occurs, you can add Metadata to the template to provide test values to use.
|
215
|
+
|
216
|
+
```
|
217
|
+
Resources:
|
218
|
+
MediaPackageV2Channel:
|
219
|
+
Type: AWS::MediaPackageV2::Channel
|
220
|
+
Metadata:
|
221
|
+
Cloud-Radar:
|
222
|
+
attribute-values:
|
223
|
+
# Default behaviour of a string is not good enough here, the attribute value is expected to be a List.
|
224
|
+
IngestEndpointUrls:
|
225
|
+
- http://one.example.com
|
226
|
+
- http://two.example.com
|
227
|
+
Properties:
|
228
|
+
ChannelGroupName: dev_video_1
|
229
|
+
ChannelName: !Sub ${AWS::StackName}-MediaPackageChannel
|
230
|
+
```
|
231
|
+
|
214
232
|
A real unit testing example using Pytest can be seen [here](./tests/test_cf/test_examples/test_unit.py)
|
215
233
|
|
216
234
|
</details>
|
@@ -307,7 +325,6 @@ A real functional testing example using Pytest can be seen [here](./tests/test_c
|
|
307
325
|
### Unit
|
308
326
|
- Add full functionality to pseudo variables.
|
309
327
|
* Variables like `Partition`, `URLSuffix` should change if the region changes.
|
310
|
-
* Variables like `StackName` and `StackId` should have a better default than ""
|
311
328
|
- Handle References to resources that shouldn't exist.
|
312
329
|
* It's currently possible that a `!Ref` to a Resource stays in the final template even if that resource is later removed because of a conditional.
|
313
330
|
|
@@ -343,11 +360,12 @@ Levi - [@shady_cuz](https://twitter.com/shady_cuz)
|
|
343
360
|
* [Taskcat](https://aws-quickstart.github.io/taskcat/)
|
344
361
|
* [Hypermodern Python](https://cjolowicz.github.io/posts/hypermodern-python-01-setup/)
|
345
362
|
* [Best-README-Template](https://github.com/othneildrew/Best-README-Template)
|
346
|
-
* @dhutchison - He was the first contributor to this project and finished the last couple of features to make this project complete. Thank you!
|
363
|
+
* [David Hutchison (@dhutchison)](https://github.com/dhutchison) - He was the first contributor to this project and finished the last couple of features to make this project complete. Thank you!
|
347
364
|
|
348
365
|
<!-- MARKDOWN LINKS & IMAGES -->
|
349
366
|
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
350
367
|
[python-shield]: https://img.shields.io/pypi/pyversions/cloud-radar?style=for-the-badge
|
368
|
+
[py-versions-shield]: https://img.shields.io/pypi/pyversions/cloud-radar?style=for-the-badge
|
351
369
|
[version-shield]: https://img.shields.io/pypi/v/cloud-radar?label=latest&style=for-the-badge
|
352
370
|
[pypi-url]: https://pypi.org/project/cloud-radar/
|
353
371
|
[test-shield]: https://img.shields.io/github/actions/workflow/status/DontShaveTheYak/cloud-radar/test.yml?label=Tests&style=for-the-badge
|
@@ -1,39 +1,45 @@
|
|
1
|
-
[
|
1
|
+
[project]
|
2
2
|
name = "cloud-radar"
|
3
|
-
version = "0.
|
3
|
+
version = "0.14.0"
|
4
4
|
description = "Run functional tests on cloudformation stacks."
|
5
5
|
readme = "README.md"
|
6
|
-
authors = [
|
6
|
+
authors = [
|
7
|
+
{ name = "Levi Blaney", email = "shadycuz+dev@gmail.com" },
|
8
|
+
{ name = "David Hutchison", email = "david@devwithimagination.com" }
|
9
|
+
]
|
7
10
|
license = "Apache-2.0"
|
8
|
-
repository = "https://github.com/DontShaveTheYak/cloud-radar"
|
9
11
|
keywords = ["aws", "cloudformation", "cloud-radar", "testing", "taskcat", "cloud", "radar"]
|
12
|
+
requires-python = ">=3.9,<4.0"
|
13
|
+
dynamic = [ "classifiers" ]
|
14
|
+
dependencies = [
|
15
|
+
"taskcat >=0.9.41, <1.0.0",
|
16
|
+
"cfn-flip >=1.3.0, <2.0.0",
|
17
|
+
"botocore >=1.35.36, <2.0.0",
|
18
|
+
]
|
19
|
+
[project.urls]
|
20
|
+
Repository = "https://github.com/DontShaveTheYak/cloud-radar"
|
21
|
+
Issues = "https://github.com/DontShaveTheYak/cloud-radar/issues"
|
22
|
+
Changelog = "https://github.com/DontShaveTheYak/cloud-radar/releases"
|
23
|
+
|
24
|
+
|
25
|
+
[tool.poetry]
|
26
|
+
requires-poetry = ">=2.0"
|
10
27
|
classifiers = [
|
11
28
|
"Development Status :: 2 - Pre-Alpha",
|
12
|
-
"License :: OSI Approved :: Apache Software License",
|
13
29
|
"Operating System :: OS Independent",
|
14
|
-
"Programming Language :: Python :: 3",
|
15
|
-
"Programming Language :: Python :: 3.9",
|
16
|
-
"Programming Language :: Python :: 3.10",
|
17
|
-
"Programming Language :: Python :: 3.11",
|
18
|
-
"Programming Language :: Python :: 3.12",
|
19
|
-
"Programming Language :: Python :: 3.13",
|
20
30
|
"Topic :: Software Development :: Libraries",
|
21
31
|
"Topic :: Software Development :: Testing"
|
22
32
|
]
|
23
33
|
|
24
|
-
|
25
|
-
python = ">=3.9,<3.14"
|
26
|
-
taskcat = "^0.9.41"
|
27
|
-
cfn-flip = "^1.3.0"
|
28
|
-
botocore = {version = ">=1.35.36", python = ">=3.13"}
|
34
|
+
|
29
35
|
|
30
36
|
[tool.poetry.group.dev.dependencies]
|
31
37
|
pytest = "^8.0.0"
|
32
38
|
coverage = {extras = ["toml"], version = "^7.0.0"}
|
33
39
|
pytest-cov = "^6.0.0"
|
34
40
|
pytest-mock = "^3.6.1"
|
35
|
-
isort = "^
|
36
|
-
black = "^
|
41
|
+
isort = "^6.0.0"
|
42
|
+
black = "^25.0.0"
|
37
43
|
flake8 = "^7.0.0"
|
38
44
|
flake8-black = "^0.3.0"
|
39
45
|
flake8-isort = "^6.1.0"
|
@@ -41,8 +47,11 @@ flake8-bugbear = "^24.0.0"
|
|
41
47
|
mypy = "^1.0.0"
|
42
48
|
types-requests = "^2.28.11"
|
43
49
|
types-PyYAML = "^6.0.12"
|
44
|
-
cfn-lint = "1.
|
45
|
-
setuptools = {version = "75.
|
50
|
+
cfn-lint = "1.25.1"
|
51
|
+
setuptools = {version = "75.8.0", python = ">=3.12"}
|
52
|
+
|
53
|
+
[tool.poetry.requires-plugins]
|
54
|
+
poetry-plugin-export = ">=1.8"
|
46
55
|
|
47
56
|
[tool.coverage.paths]
|
48
57
|
source = ["src", "*/site-packages"]
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import re
|
5
|
+
import uuid
|
5
6
|
from pathlib import Path
|
6
7
|
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union
|
7
8
|
|
@@ -26,8 +27,8 @@ class Template:
|
|
26
27
|
NoValue: str = "" # Not yet implemented
|
27
28
|
Partition: str = "aws" # Other regions not implemented
|
28
29
|
Region: str = "us-east-1"
|
29
|
-
StackId: str = "" #
|
30
|
-
StackName: str = ""
|
30
|
+
StackId: str = "" # If left blank this will be generated
|
31
|
+
StackName: str = "my-cloud-radar-stack"
|
31
32
|
URLSuffix: str = "amazonaws.com" # Other regions not implemented
|
32
33
|
|
33
34
|
def __init__(
|
@@ -80,6 +81,7 @@ class Template:
|
|
80
81
|
self.transforms: Optional[Union[str, List[str]]] = self.template.get(
|
81
82
|
"Transform", None
|
82
83
|
)
|
84
|
+
self.allowed_functions: functions.Dispatch = self.load_allowed_functions()
|
83
85
|
|
84
86
|
# All loaded, validate against any template level hooks
|
85
87
|
# that have been configured
|
@@ -206,7 +208,7 @@ class Template:
|
|
206
208
|
# If we get this far then we do not support this type of configuration file
|
207
209
|
raise ValueError("Parameter file is not in a supported format")
|
208
210
|
|
209
|
-
def load_allowed_functions(self)
|
211
|
+
def load_allowed_functions(self):
|
210
212
|
"""Loads the allowed functions for this template.
|
211
213
|
|
212
214
|
Raises:
|
@@ -230,8 +232,9 @@ class Template:
|
|
230
232
|
return {**functions.ALL_FUNCTIONS, **transform_functions}
|
231
233
|
|
232
234
|
if isinstance(self.transforms, list):
|
233
|
-
|
235
|
+
|
234
236
|
transform_functions = {}
|
237
|
+
|
235
238
|
for transform in self.transforms:
|
236
239
|
if transform not in functions.TRANSFORMS:
|
237
240
|
raise ValueError(f"Transform {transform} not supported")
|
@@ -248,13 +251,8 @@ class Template:
|
|
248
251
|
def render_all_sections(self, template: Dict[str, Any]) -> Dict[str, Any]:
|
249
252
|
"""Solves all conditionals, references and pseudo variables for all sections"""
|
250
253
|
|
251
|
-
allowed_functions = self.load_allowed_functions()
|
252
|
-
|
253
254
|
if "Conditions" in template:
|
254
|
-
template["Conditions"] = self.resolve_values(
|
255
|
-
template["Conditions"],
|
256
|
-
allowed_functions,
|
257
|
-
)
|
255
|
+
template["Conditions"] = self.resolve_values(template["Conditions"])
|
258
256
|
|
259
257
|
template_sections = ["Resources", "Outputs"]
|
260
258
|
|
@@ -277,10 +275,7 @@ class Template:
|
|
277
275
|
if not condition_value:
|
278
276
|
continue
|
279
277
|
|
280
|
-
template[section][r_name] = self.resolve_values(
|
281
|
-
r_value,
|
282
|
-
allowed_functions,
|
283
|
-
)
|
278
|
+
template[section][r_name] = self.resolve_values(r_value)
|
284
279
|
|
285
280
|
return template
|
286
281
|
|
@@ -310,6 +305,19 @@ class Template:
|
|
310
305
|
|
311
306
|
return template
|
312
307
|
|
308
|
+
# If the StackId variable is not set, generate a value for it
|
309
|
+
def _get_populated_stack_id(self) -> str:
|
310
|
+
if not Template.StackId:
|
311
|
+
# Not explicitly set, generate a value
|
312
|
+
unique_uuid = uuid.uuid4()
|
313
|
+
|
314
|
+
return (
|
315
|
+
f"arn:{Template.Partition}:cloudformation:{self.Region}:"
|
316
|
+
f"{Template.AccountId}:stack/{Template.StackName}/{unique_uuid}"
|
317
|
+
)
|
318
|
+
|
319
|
+
return Template.StackId
|
320
|
+
|
313
321
|
def create_stack(
|
314
322
|
self,
|
315
323
|
params: Optional[Dict[str, str]] = None,
|
@@ -318,6 +326,7 @@ class Template:
|
|
318
326
|
):
|
319
327
|
if region:
|
320
328
|
self.Region = region
|
329
|
+
self.StackId = self._get_populated_stack_id()
|
321
330
|
|
322
331
|
self.render(params, parameters_file=parameters_file)
|
323
332
|
|
@@ -331,7 +340,6 @@ class Template:
|
|
331
340
|
def resolve_values( # noqa: max-complexity: 13
|
332
341
|
self,
|
333
342
|
data: Any,
|
334
|
-
allowed_func: functions.Dispatch,
|
335
343
|
) -> Any:
|
336
344
|
"""Recurses through a Cloudformation template. Solving all
|
337
345
|
references and variables along the way.
|
@@ -351,10 +359,7 @@ class Template:
|
|
351
359
|
# This takes care of keys that not intrinsic functions,
|
352
360
|
# except for the condition func
|
353
361
|
if "Fn::" not in key and key != "Condition":
|
354
|
-
data[key] = self.resolve_values(
|
355
|
-
value,
|
356
|
-
allowed_func,
|
357
|
-
)
|
362
|
+
data[key] = self.resolve_values(value)
|
358
363
|
continue
|
359
364
|
|
360
365
|
# Takes care of the tricky 'Condition' key
|
@@ -372,21 +377,15 @@ class Template:
|
|
372
377
|
return functions.condition(self, value)
|
373
378
|
|
374
379
|
# Normal key like in an IAM role
|
375
|
-
data[key] = self.resolve_values(
|
376
|
-
value,
|
377
|
-
allowed_func,
|
378
|
-
)
|
380
|
+
data[key] = self.resolve_values(value)
|
379
381
|
continue
|
380
382
|
|
381
|
-
if key not in
|
383
|
+
if key not in self.allowed_functions:
|
382
384
|
raise ValueError(f"{key} with value ({value}) not allowed here")
|
383
385
|
|
384
|
-
value = self.resolve_values(
|
385
|
-
value,
|
386
|
-
functions.ALLOWED_FUNCTIONS[key],
|
387
|
-
)
|
386
|
+
value = self.resolve_values(value)
|
388
387
|
|
389
|
-
funct_result =
|
388
|
+
funct_result = self.allowed_functions[key](self, value)
|
390
389
|
|
391
390
|
if isinstance(funct_result, str):
|
392
391
|
# If the result is a string then process any
|
@@ -397,13 +396,7 @@ class Template:
|
|
397
396
|
|
398
397
|
return data
|
399
398
|
elif isinstance(data, list):
|
400
|
-
return [
|
401
|
-
self.resolve_values(
|
402
|
-
item,
|
403
|
-
allowed_func,
|
404
|
-
)
|
405
|
-
for item in data
|
406
|
-
]
|
399
|
+
return [self.resolve_values(item) for item in data]
|
407
400
|
elif isinstance(data, str):
|
408
401
|
return self.resolve_dynamic_references(data)
|
409
402
|
else:
|
@@ -283,9 +283,7 @@ def condition(template: "Template", name: Any) -> bool:
|
|
283
283
|
condition_value = template.template["Conditions"][name]
|
284
284
|
|
285
285
|
if not isinstance(condition_value, bool):
|
286
|
-
condition_value: bool = template.resolve_values( # type: ignore
|
287
|
-
condition_value, allowed_func=ALLOWED_NESTED_CONDITIONS
|
288
|
-
)
|
286
|
+
condition_value: bool = template.resolve_values(condition_value) # type: ignore
|
289
287
|
|
290
288
|
return condition_value
|
291
289
|
|
@@ -455,7 +453,18 @@ def get_att(template: "Template", values: Any) -> str:
|
|
455
453
|
if resource_name not in template.template["Resources"]:
|
456
454
|
raise KeyError(f"Fn::GetAtt - Resource {resource_name} not found in template.")
|
457
455
|
|
458
|
-
|
456
|
+
# Get the resource definition
|
457
|
+
resource = template.template["Resources"][resource_name]
|
458
|
+
|
459
|
+
# Check if there is a value in the resource Metadata for this attribute.
|
460
|
+
# If the attribute requested is in the metadata, return it.
|
461
|
+
# Otherwise use the string value of "{resource_name}.{att_name}"
|
462
|
+
|
463
|
+
metadata = resource.get("Metadata", {})
|
464
|
+
cloud_radar_metadata = metadata.get("Cloud-Radar", {})
|
465
|
+
attribute_values = cloud_radar_metadata.get("attribute-values", {})
|
466
|
+
|
467
|
+
return attribute_values.get(att_name, f"{resource_name}.{att_name}")
|
459
468
|
|
460
469
|
|
461
470
|
def get_azs(_t: "Template", region: Any) -> List[str]:
|
@@ -931,106 +940,6 @@ ALL_FUNCTIONS: Dispatch = {
|
|
931
940
|
**INTRINSICS,
|
932
941
|
}
|
933
942
|
|
934
|
-
ALLOWED_NESTED_CONDITIONS: Dispatch = {
|
935
|
-
"Fn::FindInMap": find_in_map,
|
936
|
-
"Ref": ref,
|
937
|
-
**CONDITIONS,
|
938
|
-
}
|
939
|
-
|
940
|
-
# Cloudformation only allows certain functions to be called from inside
|
941
|
-
# other functions. The keys are the function name and the values are the
|
942
|
-
# functions that are allowed to be nested inside it.
|
943
|
-
ALLOWED_FUNCTIONS: Dict[str, Dispatch] = {
|
944
|
-
"Fn::And": ALLOWED_NESTED_CONDITIONS,
|
945
|
-
"Fn::Equals": {**ALLOWED_NESTED_CONDITIONS, "Fn::Join": join, "Fn::Select": select},
|
946
|
-
"Fn::If": {
|
947
|
-
"Fn::Base64": base64,
|
948
|
-
"Fn::FindInMap": find_in_map,
|
949
|
-
"Fn::GetAtt": get_att,
|
950
|
-
"Fn::GetAZs": get_azs,
|
951
|
-
"Fn::If": if_,
|
952
|
-
"Fn::Join": join,
|
953
|
-
"Fn::Select": select,
|
954
|
-
"Fn::Sub": sub,
|
955
|
-
"Ref": ref,
|
956
|
-
"Fn::ImportValue": import_value,
|
957
|
-
},
|
958
|
-
"Fn::Not": ALLOWED_NESTED_CONDITIONS,
|
959
|
-
"Fn::Or": ALLOWED_NESTED_CONDITIONS,
|
960
|
-
"Condition": {}, # Only allows strings
|
961
|
-
"Fn::Base64": ALL_FUNCTIONS,
|
962
|
-
"Fn::Cidr": {
|
963
|
-
"Fn::Select": select,
|
964
|
-
"Ref": ref,
|
965
|
-
},
|
966
|
-
"Fn::FindInMap": {
|
967
|
-
"Fn::FindInMap": find_in_map,
|
968
|
-
"Ref": ref,
|
969
|
-
},
|
970
|
-
"Fn::GetAtt": {}, # This one is complicated =/
|
971
|
-
"Fn::GetAZs": {
|
972
|
-
"Ref": ref,
|
973
|
-
},
|
974
|
-
"Fn::ImportValue": {
|
975
|
-
"Fn::Base64": base64,
|
976
|
-
"Fn::FindInMap": find_in_map,
|
977
|
-
"Fn::If": if_,
|
978
|
-
"Fn::Join": join,
|
979
|
-
"Fn::Select": select,
|
980
|
-
"Fn::Split": split,
|
981
|
-
"Fn::Sub": sub,
|
982
|
-
"Ref": ref,
|
983
|
-
}, # Import value can't depend on resources (not implemented)
|
984
|
-
"Fn::Join": {
|
985
|
-
"Fn::Base64": base64,
|
986
|
-
"Fn::FindInMap": find_in_map,
|
987
|
-
"Fn::GetAtt": get_att,
|
988
|
-
"Fn::GetAZs": get_azs,
|
989
|
-
"Fn::If": if_,
|
990
|
-
"Fn::ImportValue": import_value,
|
991
|
-
"Fn::Join": join,
|
992
|
-
"Fn::Split": split,
|
993
|
-
"Fn::Select": select,
|
994
|
-
"Fn::Sub": sub,
|
995
|
-
"Ref": ref,
|
996
|
-
},
|
997
|
-
"Fn::Select": {
|
998
|
-
"Fn::FindInMap": find_in_map,
|
999
|
-
"Fn::GetAtt": get_att,
|
1000
|
-
"Fn::GetAZs": get_azs,
|
1001
|
-
"Fn::If": if_,
|
1002
|
-
"Fn::Split": split,
|
1003
|
-
"Ref": ref,
|
1004
|
-
},
|
1005
|
-
"Fn::Split": {
|
1006
|
-
"Fn::Base64": base64,
|
1007
|
-
"Fn::FindInMap": find_in_map,
|
1008
|
-
"Fn::GetAtt": get_att,
|
1009
|
-
"Fn::GetAZs": get_azs,
|
1010
|
-
"Fn::If": if_,
|
1011
|
-
"Fn::ImportValue": import_value,
|
1012
|
-
"Fn::Join": join,
|
1013
|
-
"Fn::Split": split,
|
1014
|
-
"Fn::Select": select,
|
1015
|
-
"Fn::Sub": sub,
|
1016
|
-
"Ref": ref,
|
1017
|
-
},
|
1018
|
-
"Fn::Sub": {
|
1019
|
-
"Fn::Base64": base64,
|
1020
|
-
"Fn::FindInMap": find_in_map,
|
1021
|
-
"Fn::GetAtt": get_att,
|
1022
|
-
"Fn::GetAZs": get_azs,
|
1023
|
-
"Fn::If": if_,
|
1024
|
-
"Fn::ImportValue": import_value,
|
1025
|
-
"Fn::Join": join,
|
1026
|
-
"Fn::Select": select,
|
1027
|
-
"Ref": ref,
|
1028
|
-
"Fn::Sub": sub,
|
1029
|
-
},
|
1030
|
-
"Fn::Transform": {}, # Transform isn't fully implemented
|
1031
|
-
"Ref": {}, # String only.
|
1032
|
-
}
|
1033
|
-
|
1034
943
|
# Extra functions that are allowed if the template is using a transform.
|
1035
944
|
TRANSFORMS: Dict[str, Dispatch] = {
|
1036
945
|
"AWS::CodeDeployBlueGreen": {},
|
@@ -7,40 +7,38 @@ from cloud_radar.cf.unit._template import Template
|
|
7
7
|
|
8
8
|
def test_load_allowed_functions_no_transforms():
|
9
9
|
template = Template({})
|
10
|
-
|
11
|
-
assert
|
10
|
+
template.load_allowed_functions()
|
11
|
+
assert template.allowed_functions == functions.ALL_FUNCTIONS
|
12
12
|
|
13
13
|
|
14
14
|
def test_load_allowed_functions_single_transform():
|
15
15
|
template = Template({"Transform": "AWS::Serverless-2016-10-31"})
|
16
|
-
|
16
|
+
template.load_allowed_functions()
|
17
17
|
expected = {
|
18
18
|
**functions.ALL_FUNCTIONS,
|
19
19
|
**functions.TRANSFORMS["AWS::Serverless-2016-10-31"],
|
20
20
|
}
|
21
|
-
assert
|
21
|
+
assert template.allowed_functions == expected
|
22
22
|
|
23
23
|
|
24
24
|
def test_load_allowed_functions_multiple_transforms():
|
25
25
|
template = Template({"Transform": ["AWS::Serverless-2016-10-31", "AWS::Include"]})
|
26
|
-
|
26
|
+
template.load_allowed_functions()
|
27
27
|
expected = {
|
28
28
|
**functions.ALL_FUNCTIONS,
|
29
29
|
**functions.TRANSFORMS["AWS::Serverless-2016-10-31"],
|
30
30
|
**functions.TRANSFORMS["AWS::Include"],
|
31
31
|
}
|
32
|
-
assert
|
32
|
+
assert template.allowed_functions == expected
|
33
33
|
|
34
34
|
|
35
35
|
def test_load_allowed_functions_invalid_transform():
|
36
|
-
|
36
|
+
|
37
37
|
with pytest.raises(ValueError):
|
38
|
-
|
38
|
+
Template({"Transform": "InvalidTransform"})
|
39
39
|
|
40
40
|
|
41
41
|
def test_load_allowed_functions_invalid_transforms():
|
42
|
-
|
43
|
-
{"Transform": ["AWS::Serverless-2016-10-31", "InvalidTransform"]}
|
44
|
-
)
|
42
|
+
|
45
43
|
with pytest.raises(ValueError):
|
46
|
-
|
44
|
+
Template({"Transform": ["AWS::Serverless-2016-10-31", "InvalidTransform"]})
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|