cfn-check 0.3.3__py3-none-any.whl → 0.5.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.
Potentially problematic release.
This version of cfn-check might be problematic. Click here for more details.
- cfn_check/cli/render.py +83 -0
- cfn_check/cli/root.py +3 -1
- cfn_check/cli/utils/attributes.py +1 -1
- cfn_check/cli/utils/files.py +46 -20
- cfn_check/cli/validate.py +8 -1
- cfn_check/collection/collection.py +58 -1
- cfn_check/evaluation/evaluator.py +5 -1
- cfn_check/rendering/__init__.py +1 -0
- cfn_check/rendering/renderer.py +740 -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.3.dist-info → cfn_check-0.5.0.dist-info}/METADATA +1 -1
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/RECORD +21 -17
- example/multitag.py +21 -0
- example/pydantic_rules.py +102 -3
- example/renderer_test.py +42 -0
- cfn_check/loader/__init__.py +0 -0
- cfn_check/loader/loader.py +0 -21
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/WHEEL +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.3.3.dist-info → cfn_check-0.5.0.dist-info}/top_level.txt +0 -0
cfn_check/cli/render.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
from async_logging import LogLevelName, Logger, LoggingConfig
|
|
3
|
+
from cocoa.cli import CLI
|
|
4
|
+
|
|
5
|
+
from cfn_check.cli.utils.files import load_templates, write_to_file
|
|
6
|
+
from cfn_check.rendering import Renderer
|
|
7
|
+
from cfn_check.logging.models import InfoLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@CLI.command(
|
|
11
|
+
display_help_on_error=False
|
|
12
|
+
)
|
|
13
|
+
async def render(
|
|
14
|
+
path: str,
|
|
15
|
+
output_file: str = 'rendered.yml',
|
|
16
|
+
parameters: list[str] | None = None,
|
|
17
|
+
references: list[str] | None = None,
|
|
18
|
+
tags: list[str] = [
|
|
19
|
+
'Ref',
|
|
20
|
+
'Sub',
|
|
21
|
+
'Join',
|
|
22
|
+
'Select',
|
|
23
|
+
'Split',
|
|
24
|
+
'GetAtt',
|
|
25
|
+
'GetAZs',
|
|
26
|
+
'ImportValue',
|
|
27
|
+
'Equals',
|
|
28
|
+
'If',
|
|
29
|
+
'Not',
|
|
30
|
+
'And',
|
|
31
|
+
'Or',
|
|
32
|
+
'Condition',
|
|
33
|
+
'FindInMap',
|
|
34
|
+
],
|
|
35
|
+
log_level: LogLevelName = 'info',
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Render a Cloud Formation template
|
|
39
|
+
|
|
40
|
+
@param output_file Path to output the rendered CloudFormation template to
|
|
41
|
+
@param parameters A list of <key>=<value> input Parameters to use
|
|
42
|
+
@param references A list of <key>=<value> input !Ref values to use
|
|
43
|
+
@param tags List of CloudFormation intrinsic function tags
|
|
44
|
+
@param log_level The log level to use
|
|
45
|
+
"""
|
|
46
|
+
logging_config = LoggingConfig()
|
|
47
|
+
logging_config.update(
|
|
48
|
+
log_level=log_level,
|
|
49
|
+
log_output='stderr',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parsed_parameters: dict[str, str] | None = None
|
|
53
|
+
if parameters:
|
|
54
|
+
parsed_parameters = dict([
|
|
55
|
+
parameter.split('=', maxsplit=1) for parameter in parameters if len(parameter.split('=', maxsplit=1)) > 0
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
parsed_references: dict[str, str] | None = None
|
|
59
|
+
if references:
|
|
60
|
+
parsed_references = dict([
|
|
61
|
+
reference.split('=', maxsplit=1) for reference in references if len(reference.split('=', maxsplit=1)) > 0
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
logger = Logger()
|
|
65
|
+
|
|
66
|
+
templates = await load_templates(
|
|
67
|
+
path,
|
|
68
|
+
tags,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert len(templates) == 1 , '❌ Can only render one file'
|
|
72
|
+
|
|
73
|
+
_, template = templates[0]
|
|
74
|
+
renderer = Renderer()
|
|
75
|
+
rendered = renderer.render(
|
|
76
|
+
template,
|
|
77
|
+
parameters=parsed_parameters,
|
|
78
|
+
references=parsed_references,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
await write_to_file(output_file, rendered)
|
|
82
|
+
|
|
83
|
+
await logger.log(InfoLog(message=f'✅ {path} template rendered'))
|
cfn_check/cli/root.py
CHANGED
|
@@ -7,6 +7,7 @@ from cocoa.ui.components.terminal import Section, SectionConfig
|
|
|
7
7
|
from cocoa.ui.components.header import Header, HeaderConfig
|
|
8
8
|
from cocoa.ui.components.terminal import Terminal, EngineConfig
|
|
9
9
|
|
|
10
|
+
from .render import render
|
|
10
11
|
from .validate import validate
|
|
11
12
|
|
|
12
13
|
async def create_header(
|
|
@@ -47,7 +48,8 @@ async def create_header(
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
@CLI.root(
|
|
51
|
+
@CLI.root(
|
|
52
|
+
render,
|
|
51
53
|
validate,
|
|
52
54
|
global_styles=CLIStyle(
|
|
53
55
|
header=create_header,
|
|
@@ -4,7 +4,7 @@ from cfn_check.validation.validator import Validator
|
|
|
4
4
|
|
|
5
5
|
def bind(
|
|
6
6
|
rule_set: Collection,
|
|
7
|
-
validation: Validator
|
|
7
|
+
validation: Validator,
|
|
8
8
|
):
|
|
9
9
|
validation.func = validation.func.__get__(rule_set, rule_set.__class__)
|
|
10
10
|
setattr(rule_set, validation.func.__name__, validation.func)
|
cfn_check/cli/utils/files.py
CHANGED
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import os
|
|
4
4
|
import pathlib
|
|
5
|
-
import
|
|
6
|
-
from cfn_check.loader.loader import (
|
|
7
|
-
Loader,
|
|
8
|
-
create_tag,
|
|
9
|
-
find_templates,
|
|
10
|
-
)
|
|
5
|
+
from ruamel.yaml import YAML
|
|
11
6
|
from cfn_check.shared.types import YamlObject, Data
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
|
|
9
|
+
def find_templates(path, file_pattern):
|
|
10
|
+
return list(pathlib.Path(path).rglob(file_pattern))
|
|
11
|
+
|
|
12
|
+
def open_template(path: str) -> tuple[str, YamlObject] | None:
|
|
14
13
|
|
|
15
14
|
if os.path.exists(path) is False:
|
|
16
15
|
return None
|
|
17
16
|
|
|
18
17
|
try:
|
|
19
|
-
with open(path, 'r') as
|
|
20
|
-
|
|
18
|
+
with open(path, 'r') as yml:
|
|
19
|
+
loader = YAML(typ='rt')
|
|
20
|
+
loader.preserve_quotes = True
|
|
21
|
+
loader.indent(mapping=2, sequence=4, offset=2)
|
|
22
|
+
return (path, loader.load(yml))
|
|
21
23
|
except Exception as e:
|
|
22
24
|
raise e
|
|
23
25
|
|
|
@@ -38,6 +40,16 @@ async def convert_to_cwd(loop: asyncio.AbstractEventLoop):
|
|
|
38
40
|
os.getcwd,
|
|
39
41
|
)
|
|
40
42
|
|
|
43
|
+
async def convert_to_absolute(path: str, loop: asyncio.AbstractEventLoop) -> str:
|
|
44
|
+
abspath = pathlib.Path(path)
|
|
45
|
+
|
|
46
|
+
return str(
|
|
47
|
+
await loop.run_in_executor(
|
|
48
|
+
None,
|
|
49
|
+
abspath.absolute,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
41
53
|
async def localize_path(path: str, loop: asyncio.AbstractEventLoop):
|
|
42
54
|
localized = path.replace('~/', '')
|
|
43
55
|
|
|
@@ -89,17 +101,7 @@ async def load_templates(
|
|
|
89
101
|
|
|
90
102
|
assert len(template_filepaths) > 0 , '❌ No matching files found'
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
new_tag = await loop.run_in_executor(
|
|
94
|
-
None,
|
|
95
|
-
create_tag,
|
|
96
|
-
tag,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
Loader.add_constructor(f'!{tag}', new_tag)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
templates: list[Data] = await asyncio.gather(*[
|
|
104
|
+
templates: list[tuple[str, Data]] = await asyncio.gather(*[
|
|
103
105
|
loop.run_in_executor(
|
|
104
106
|
None,
|
|
105
107
|
open_template,
|
|
@@ -114,3 +116,27 @@ async def load_templates(
|
|
|
114
116
|
assert len(found_templates) > 0, "❌ Could not open any templates"
|
|
115
117
|
|
|
116
118
|
return templates
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def write_to_file(path: str, data: YamlObject):
|
|
122
|
+
loop = asyncio.get_event_loop()
|
|
123
|
+
|
|
124
|
+
if path.startswith('~/'):
|
|
125
|
+
path = await localize_path(path, loop)
|
|
126
|
+
|
|
127
|
+
output_path = await convert_to_absolute(path, loop)
|
|
128
|
+
|
|
129
|
+
await loop.run_in_executor(
|
|
130
|
+
None,
|
|
131
|
+
_write_to_file,
|
|
132
|
+
output_path,
|
|
133
|
+
data,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _write_to_file(path: str, data: YamlObject):
|
|
137
|
+
dumper = YAML(typ='rt')
|
|
138
|
+
dumper.preserve_quotes = True
|
|
139
|
+
dumper.width = 4096
|
|
140
|
+
dumper.indent(mapping=2, sequence=4, offset=2)
|
|
141
|
+
with open(path, 'w') as yml:
|
|
142
|
+
dumper.dump(data, yml)
|
cfn_check/cli/validate.py
CHANGED
|
@@ -58,6 +58,11 @@ async def validate(
|
|
|
58
58
|
file_pattern=file_pattern,
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
+
for file, data in templates:
|
|
62
|
+
for name, rule in rules.data.items():
|
|
63
|
+
rules.data[name] = rule()
|
|
64
|
+
rules.data[name].documents[file] = data
|
|
65
|
+
|
|
61
66
|
validation_set = ValidationSet([
|
|
62
67
|
bind(
|
|
63
68
|
rule,
|
|
@@ -68,7 +73,9 @@ async def validate(
|
|
|
68
73
|
if isinstance(validation, Validator)
|
|
69
74
|
])
|
|
70
75
|
|
|
71
|
-
if validation_error := validation_set.validate(
|
|
76
|
+
if validation_error := validation_set.validate([
|
|
77
|
+
template_data for _, template_data in templates
|
|
78
|
+
]):
|
|
72
79
|
raise validation_error
|
|
73
80
|
|
|
74
81
|
templates_evaluated = len(templates)
|
|
@@ -1,2 +1,59 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
from pydantic import ValidationError
|
|
3
|
+
|
|
4
|
+
from cfn_check.shared.types import Data
|
|
5
|
+
from cfn_check.evaluation.evaluator import Evaluator
|
|
6
|
+
|
|
1
7
|
class Collection:
|
|
2
|
-
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.documents: dict[str, Data] = {}
|
|
11
|
+
self._evaluator = Evaluator()
|
|
12
|
+
|
|
13
|
+
def query(
|
|
14
|
+
self,
|
|
15
|
+
query: str,
|
|
16
|
+
document: str | None = None,
|
|
17
|
+
filters: list[Callable[[Data], Data]] | None = None
|
|
18
|
+
) -> list[Data] | None:
|
|
19
|
+
|
|
20
|
+
if document and (
|
|
21
|
+
document_data := self.documents.get(document)
|
|
22
|
+
):
|
|
23
|
+
return self._evaluator.match(
|
|
24
|
+
document_data,
|
|
25
|
+
query,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
results: list[tuple[str, Data]] = []
|
|
29
|
+
|
|
30
|
+
for document_data in self.documents.values():
|
|
31
|
+
result = self._evaluator.match(
|
|
32
|
+
document_data,
|
|
33
|
+
query,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
results.extend(result)
|
|
37
|
+
|
|
38
|
+
filtered: list[Data] = []
|
|
39
|
+
if filters:
|
|
40
|
+
try:
|
|
41
|
+
for _, found in results:
|
|
42
|
+
for filter in filters:
|
|
43
|
+
found = filter(found)
|
|
44
|
+
|
|
45
|
+
if found is None:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if found:
|
|
49
|
+
filtered.append(found)
|
|
50
|
+
|
|
51
|
+
return filtered
|
|
52
|
+
|
|
53
|
+
except ValidationError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
found for _, found in results
|
|
58
|
+
]
|
|
59
|
+
|
|
@@ -7,12 +7,14 @@ from cfn_check.shared.types import (
|
|
|
7
7
|
YamlObject,
|
|
8
8
|
)
|
|
9
9
|
|
|
10
|
+
from cfn_check.rendering import Renderer
|
|
10
11
|
from .parsing import QueryParser
|
|
11
12
|
|
|
12
13
|
class Evaluator:
|
|
13
14
|
|
|
14
15
|
def __init__(self):
|
|
15
16
|
self._query_parser = QueryParser()
|
|
17
|
+
self._renderer = Renderer()
|
|
16
18
|
|
|
17
19
|
def match(
|
|
18
20
|
self,
|
|
@@ -20,7 +22,9 @@ class Evaluator:
|
|
|
20
22
|
path: str,
|
|
21
23
|
):
|
|
22
24
|
items: Items = deque()
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
rendered = self._renderer.render(resources)
|
|
27
|
+
items.append(rendered)
|
|
24
28
|
|
|
25
29
|
segments = path.split("::")[::-1]
|
|
26
30
|
# Queries can be multi-segment,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .renderer import Renderer as Renderer
|