cfn-check 0.3.2__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of cfn-check might be problematic. Click here for more details.
- cfn_check/cli/config.py +10 -0
- cfn_check/cli/render.py +142 -0
- cfn_check/cli/root.py +3 -1
- cfn_check/cli/utils/attributes.py +1 -1
- cfn_check/cli/utils/files.py +46 -21
- cfn_check/cli/utils/stdout.py +18 -0
- cfn_check/cli/validate.py +35 -26
- cfn_check/collection/collection.py +58 -1
- cfn_check/evaluation/evaluator.py +31 -3
- cfn_check/evaluation/parsing/token.py +4 -1
- cfn_check/evaluation/validate.py +33 -2
- cfn_check/rendering/__init__.py +1 -0
- cfn_check/rendering/cidr_solver.py +66 -0
- cfn_check/rendering/renderer.py +1316 -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.2.dist-info → cfn_check-0.8.1.dist-info}/METADATA +106 -5
- cfn_check-0.8.1.dist-info/RECORD +42 -0
- example/multitag.py +21 -0
- example/pydantic_rules.py +114 -0
- example/renderer_test.py +42 -0
- cfn_check/loader/__init__.py +0 -0
- cfn_check/loader/loader.py +0 -21
- cfn_check-0.3.2.dist-info/RECORD +0 -34
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/WHEEL +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/entry_points.txt +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {cfn_check-0.3.2.dist-info → cfn_check-0.8.1.dist-info}/top_level.txt +0 -0
cfn_check/cli/config.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pydantic import BaseModel, StrictStr, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config(BaseModel):
|
|
5
|
+
attributes: dict[StrictStr, StrictStr] | None = None
|
|
6
|
+
availability_zones: list[StrictStr] | None = None
|
|
7
|
+
import_values: dict[StrictStr, StrictStr] | None = None
|
|
8
|
+
mappings: dict[StrictStr, StrictStr] | None = None
|
|
9
|
+
parameters: dict[StrictStr, StrictStr] | None = None
|
|
10
|
+
references: dict[StrictStr, StrictStr] | None = None
|
cfn_check/cli/render.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
from async_logging import LogLevelName, Logger, LoggingConfig
|
|
3
|
+
from cocoa.cli import CLI, YamlFile
|
|
4
|
+
from ruamel.yaml.comments import CommentedMap
|
|
5
|
+
|
|
6
|
+
from cfn_check.cli.utils.files import load_templates, write_to_file
|
|
7
|
+
from cfn_check.cli.utils.stdout import write_to_stdout
|
|
8
|
+
from cfn_check.rendering import Renderer
|
|
9
|
+
from cfn_check.logging.models import InfoLog
|
|
10
|
+
from .config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@CLI.command(
|
|
14
|
+
shortnames={
|
|
15
|
+
'availability-zones': 'z'
|
|
16
|
+
},
|
|
17
|
+
display_help_on_error=False,
|
|
18
|
+
)
|
|
19
|
+
async def render(
|
|
20
|
+
path: str,
|
|
21
|
+
config: YamlFile[Config] = 'config.yml',
|
|
22
|
+
output_file: str | None = None,
|
|
23
|
+
attributes: list[str] | None = None,
|
|
24
|
+
availability_zones: list[str] | None = None,
|
|
25
|
+
import_values: list[str] | None = None,
|
|
26
|
+
mappings: list[str] | None = None,
|
|
27
|
+
parameters: list[str] | None = None,
|
|
28
|
+
references: list[str] | None = None,
|
|
29
|
+
log_level: LogLevelName = 'info',
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Render a Cloud Formation template
|
|
33
|
+
|
|
34
|
+
@param attributes A list of <key>=<value> k/v strings for !GetAtt calls to use
|
|
35
|
+
@param availability-zones A list of <availability_zone> strings for !GetAZs calls to use
|
|
36
|
+
@param config A CFN-Check yaml config file
|
|
37
|
+
@param import-values A list of <filepath>=<export_value> k/v strings for !ImportValue
|
|
38
|
+
@param mappings A list of <key>=<value> k/v string specifying which Mappings to use
|
|
39
|
+
@param output-file Path to output the rendered CloudFormation template to
|
|
40
|
+
@param parameters A list of <key>=<value> k/v string for Parameters to use
|
|
41
|
+
@param references A list of <key>=<value> k/v string for !Ref values to use
|
|
42
|
+
@param log-level The log level to use
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
config_data = config.data
|
|
46
|
+
if config_data is None:
|
|
47
|
+
config_data = Config()
|
|
48
|
+
|
|
49
|
+
logging_config = LoggingConfig()
|
|
50
|
+
logging_config.update(
|
|
51
|
+
log_level=log_level,
|
|
52
|
+
log_output='stderr',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parsed_attributes: dict[str, str] | None = None
|
|
56
|
+
if attributes:
|
|
57
|
+
parsed_attributes = dict([
|
|
58
|
+
attribute.split('=', maxsplit=1) for attribute in attributes if len(attribute.split('=', maxsplit=1)) > 0
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
parsed_attributes.update(
|
|
62
|
+
config_data.attributes or {}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
parsed_import_values: dict[str, tuple[str, CommentedMap]] | None = None
|
|
66
|
+
if import_values:
|
|
67
|
+
parsed_import_value_pairs = dict([
|
|
68
|
+
import_value.split('=', maxsplit=1) for import_value in import_values if len(import_value.split('=', maxsplit=1)) > 0
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
parsed_import_value_pairs.update(
|
|
72
|
+
config_data.import_values or {}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parsed_import_values = {}
|
|
76
|
+
for import_file, import_key in parsed_import_value_pairs:
|
|
77
|
+
parsed_import_values[import_file] = (
|
|
78
|
+
import_key,
|
|
79
|
+
await load_templates(import_file)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parsed_mappings: dict[str, str] | None = None
|
|
83
|
+
if mappings:
|
|
84
|
+
parsed_mappings = dict([
|
|
85
|
+
mapping.split('=', maxsplit=1) for mapping in mappings if len(mapping.split('=', maxsplit=1)) > 0
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
parsed_mappings.update(
|
|
89
|
+
config_data.mappings or {}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
parsed_parameters: dict[str, str] | None = None
|
|
93
|
+
if parameters:
|
|
94
|
+
parsed_parameters = dict([
|
|
95
|
+
parameter.split('=', maxsplit=1) for parameter in parameters if len(parameter.split('=', maxsplit=1)) > 0
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
parsed_parameters.update(
|
|
99
|
+
config_data.parameters or {}
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
parsed_references: dict[str, str] | None = None
|
|
103
|
+
if references:
|
|
104
|
+
parsed_references = dict([
|
|
105
|
+
reference.split('=', maxsplit=1) for reference in references if len(reference.split('=', maxsplit=1)) > 0
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
parsed_references.update(
|
|
109
|
+
config_data.references or {}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
logger = Logger()
|
|
113
|
+
|
|
114
|
+
templates = await load_templates(
|
|
115
|
+
path,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
assert len(templates) == 1 , '❌ Can only render one file'
|
|
119
|
+
|
|
120
|
+
_, template = templates[0]
|
|
121
|
+
renderer = Renderer()
|
|
122
|
+
rendered = renderer.render(
|
|
123
|
+
template,
|
|
124
|
+
attributes=parsed_attributes,
|
|
125
|
+
availability_zones=availability_zones,
|
|
126
|
+
mappings=parsed_mappings,
|
|
127
|
+
parameters=parsed_parameters,
|
|
128
|
+
references=parsed_references,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if output_file is False:
|
|
132
|
+
await write_to_file(output_file, rendered)
|
|
133
|
+
await logger.log(InfoLog(message=f'✅ {path} template rendered'))
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
await write_to_stdout(rendered)
|
|
137
|
+
|
|
138
|
+
if config_data:
|
|
139
|
+
await write_to_file(
|
|
140
|
+
config.value,
|
|
141
|
+
config_data.model_dump(),
|
|
142
|
+
)
|
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
|
|
|
@@ -55,7 +67,6 @@ async def localize_path(path: str, loop: asyncio.AbstractEventLoop):
|
|
|
55
67
|
|
|
56
68
|
async def load_templates(
|
|
57
69
|
path: str,
|
|
58
|
-
tags: list[str],
|
|
59
70
|
file_pattern: str | None = None,
|
|
60
71
|
):
|
|
61
72
|
|
|
@@ -89,17 +100,7 @@ async def load_templates(
|
|
|
89
100
|
|
|
90
101
|
assert len(template_filepaths) > 0 , '❌ No matching files found'
|
|
91
102
|
|
|
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(*[
|
|
103
|
+
templates: list[tuple[str, Data]] = await asyncio.gather(*[
|
|
103
104
|
loop.run_in_executor(
|
|
104
105
|
None,
|
|
105
106
|
open_template,
|
|
@@ -114,3 +115,27 @@ async def load_templates(
|
|
|
114
115
|
assert len(found_templates) > 0, "❌ Could not open any templates"
|
|
115
116
|
|
|
116
117
|
return templates
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def write_to_file(path: str, data: YamlObject):
|
|
121
|
+
loop = asyncio.get_event_loop()
|
|
122
|
+
|
|
123
|
+
if path.startswith('~/'):
|
|
124
|
+
path = await localize_path(path, loop)
|
|
125
|
+
|
|
126
|
+
output_path = await convert_to_absolute(path, loop)
|
|
127
|
+
|
|
128
|
+
await loop.run_in_executor(
|
|
129
|
+
None,
|
|
130
|
+
_write_to_file,
|
|
131
|
+
output_path,
|
|
132
|
+
data,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _write_to_file(path: str, data: YamlObject):
|
|
136
|
+
dumper = YAML(typ='rt')
|
|
137
|
+
dumper.preserve_quotes = True
|
|
138
|
+
dumper.width = 4096
|
|
139
|
+
dumper.indent(mapping=2, sequence=4, offset=2)
|
|
140
|
+
with open(path, 'w') as yml:
|
|
141
|
+
dumper.dump(data, yml)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from ruamel.yaml import YAML
|
|
4
|
+
from ruamel.yaml.comments import CommentedBase
|
|
5
|
+
|
|
6
|
+
async def write_to_stdout(data: CommentedBase):
|
|
7
|
+
loop = asyncio.get_event_loop()
|
|
8
|
+
|
|
9
|
+
yaml = YAML(typ=['rt'])
|
|
10
|
+
yaml.preserve_quotes = True
|
|
11
|
+
yaml.width = 4096
|
|
12
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
13
|
+
await loop.run_in_executor(
|
|
14
|
+
None,
|
|
15
|
+
yaml.dump,
|
|
16
|
+
data,
|
|
17
|
+
sys.stdout,
|
|
18
|
+
)
|
cfn_check/cli/validate.py
CHANGED
|
@@ -1,49 +1,44 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
|
|
3
3
|
from async_logging import LogLevelName, Logger, LoggingConfig
|
|
4
|
-
from cocoa.cli import CLI, ImportType
|
|
4
|
+
from cocoa.cli import CLI, ImportType, YamlFile
|
|
5
5
|
|
|
6
6
|
from cfn_check.cli.utils.attributes import bind
|
|
7
|
-
from cfn_check.cli.utils.files import load_templates
|
|
7
|
+
from cfn_check.cli.utils.files import load_templates, write_to_file
|
|
8
8
|
from cfn_check.evaluation.validate import ValidationSet
|
|
9
9
|
from cfn_check.logging.models import InfoLog
|
|
10
10
|
from cfn_check.collection.collection import Collection
|
|
11
11
|
from cfn_check.validation.validator import Validator
|
|
12
|
+
from .config import Config
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
@CLI.command(
|
|
15
|
+
@CLI.command(
|
|
16
|
+
shortnames={
|
|
17
|
+
'flags': 'F'
|
|
18
|
+
}
|
|
19
|
+
)
|
|
15
20
|
async def validate(
|
|
16
21
|
path: str,
|
|
22
|
+
config: YamlFile[Config] = 'config.yml',
|
|
17
23
|
file_pattern: str | None = None,
|
|
18
24
|
rules: ImportType[Collection] = None,
|
|
19
|
-
|
|
20
|
-
'Ref',
|
|
21
|
-
'Sub',
|
|
22
|
-
'Join',
|
|
23
|
-
'Select',
|
|
24
|
-
'Split',
|
|
25
|
-
'GetAtt',
|
|
26
|
-
'GetAZs',
|
|
27
|
-
'ImportValue',
|
|
28
|
-
'Equals',
|
|
29
|
-
'If',
|
|
30
|
-
'Not',
|
|
31
|
-
'And',
|
|
32
|
-
'Or',
|
|
33
|
-
'Condition',
|
|
34
|
-
'FindInMap',
|
|
35
|
-
],
|
|
25
|
+
flags: list[str] | None = None,
|
|
36
26
|
log_level: LogLevelName = 'info',
|
|
37
27
|
):
|
|
38
28
|
'''
|
|
39
29
|
Validate Cloud Foundation
|
|
40
30
|
|
|
41
|
-
@param
|
|
31
|
+
@param config A CFN-Check yaml config file
|
|
32
|
+
@param disabled A list of string features to disable during checks
|
|
42
33
|
@param file_pattern A string pattern used to find template files
|
|
43
|
-
@param
|
|
34
|
+
@param rules Path to a file containing Collections
|
|
44
35
|
@param log_level The log level to use
|
|
45
36
|
'''
|
|
46
37
|
|
|
38
|
+
config_data = config.data
|
|
39
|
+
if config_data is None:
|
|
40
|
+
config_data = Config()
|
|
41
|
+
|
|
47
42
|
logging_config = LoggingConfig()
|
|
48
43
|
logging_config.update(
|
|
49
44
|
log_level=log_level,
|
|
@@ -52,12 +47,19 @@ async def validate(
|
|
|
52
47
|
|
|
53
48
|
logger = Logger()
|
|
54
49
|
|
|
50
|
+
if flags is None:
|
|
51
|
+
flags = []
|
|
52
|
+
|
|
55
53
|
templates = await load_templates(
|
|
56
54
|
path,
|
|
57
|
-
tags,
|
|
58
55
|
file_pattern=file_pattern,
|
|
59
56
|
)
|
|
60
57
|
|
|
58
|
+
for file, data in templates:
|
|
59
|
+
for name, rule in rules.data.items():
|
|
60
|
+
rules.data[name] = rule()
|
|
61
|
+
rules.data[name].documents[file] = data
|
|
62
|
+
|
|
61
63
|
validation_set = ValidationSet([
|
|
62
64
|
bind(
|
|
63
65
|
rule,
|
|
@@ -66,12 +68,19 @@ async def validate(
|
|
|
66
68
|
for rule in rules.data.values()
|
|
67
69
|
for _, validation in inspect.getmembers(rule)
|
|
68
70
|
if isinstance(validation, Validator)
|
|
69
|
-
])
|
|
71
|
+
], flags=flags)
|
|
70
72
|
|
|
71
|
-
if validation_error := validation_set.validate(
|
|
73
|
+
if validation_error := validation_set.validate([
|
|
74
|
+
template_data for _, template_data in templates
|
|
75
|
+
]):
|
|
72
76
|
raise validation_error
|
|
73
77
|
|
|
74
78
|
templates_evaluated = len(templates)
|
|
75
79
|
|
|
76
80
|
await logger.log(InfoLog(message=f'✅ {validation_set.count} validations met for {templates_evaluated} templates'))
|
|
77
|
-
|
|
81
|
+
|
|
82
|
+
if config_data:
|
|
83
|
+
await write_to_file(
|
|
84
|
+
config.value,
|
|
85
|
+
config_data.model_dump(),
|
|
86
|
+
)
|
|
@@ -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
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from collections import deque
|
|
2
|
-
from typing import Deque
|
|
2
|
+
from typing import Deque, Any
|
|
3
|
+
from ruamel.yaml.comments import CommentedMap
|
|
3
4
|
|
|
4
5
|
from cfn_check.shared.types import (
|
|
5
6
|
Data,
|
|
@@ -7,19 +8,46 @@ from cfn_check.shared.types import (
|
|
|
7
8
|
YamlObject,
|
|
8
9
|
)
|
|
9
10
|
|
|
11
|
+
from cfn_check.rendering import Renderer
|
|
10
12
|
from .parsing import QueryParser
|
|
11
13
|
|
|
12
14
|
class Evaluator:
|
|
13
15
|
|
|
14
|
-
def __init__(
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
flags: list[str] | None = None
|
|
19
|
+
):
|
|
20
|
+
if flags is None:
|
|
21
|
+
flags = []
|
|
22
|
+
|
|
23
|
+
self.flags = flags
|
|
15
24
|
self._query_parser = QueryParser()
|
|
25
|
+
self._renderer = Renderer()
|
|
16
26
|
|
|
17
27
|
def match(
|
|
18
28
|
self,
|
|
19
29
|
resources: YamlObject,
|
|
20
30
|
path: str,
|
|
31
|
+
attributes: dict[str, Any] | None = None,
|
|
32
|
+
availability_zones: list[str] | None = None,
|
|
33
|
+
import_values: dict[str, tuple[str, CommentedMap]] | None = None,
|
|
34
|
+
mappings: dict[str, str] | None = None,
|
|
35
|
+
parameters: dict[str, Any] | None = None,
|
|
36
|
+
references: dict[str, str] | None = None,
|
|
21
37
|
):
|
|
22
38
|
items: Items = deque()
|
|
39
|
+
|
|
40
|
+
if 'no-render' not in self.flags:
|
|
41
|
+
resources = self._renderer.render(
|
|
42
|
+
resources,
|
|
43
|
+
attributes=attributes,
|
|
44
|
+
availability_zones=availability_zones,
|
|
45
|
+
import_values=import_values,
|
|
46
|
+
mappings=mappings,
|
|
47
|
+
parameters=parameters,
|
|
48
|
+
references=references,
|
|
49
|
+
)
|
|
50
|
+
|
|
23
51
|
items.append(resources)
|
|
24
52
|
|
|
25
53
|
segments = path.split("::")[::-1]
|
|
@@ -47,7 +75,7 @@ class Evaluator:
|
|
|
47
75
|
|
|
48
76
|
composite_keys = updated_keys
|
|
49
77
|
|
|
50
|
-
assert len(composite_keys) == len(items), f'❌ {len(items)} returned for {len(composite_keys)} keys. Are you sure you used a range ([*]) selector?'
|
|
78
|
+
assert len(composite_keys) == len(items), f'❌ {len(items)} matches returned for {len(composite_keys)} keys. Are you sure you used a range ([*]) selector?'
|
|
51
79
|
|
|
52
80
|
results: list[tuple[str, Data]] = []
|
|
53
81
|
for idx, item in enumerate(list(items)):
|
cfn_check/evaluation/validate.py
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
from pydantic import ValidationError
|
|
4
|
+
from ruamel.yaml.comments import TaggedScalar, CommentedMap, CommentedSeq
|
|
2
5
|
|
|
3
6
|
from cfn_check.validation.validator import Validator
|
|
7
|
+
from cfn_check.shared.types import (
|
|
8
|
+
YamlObject,
|
|
9
|
+
)
|
|
10
|
+
|
|
4
11
|
from .errors import assemble_validation_error
|
|
5
12
|
from .evaluator import Evaluator
|
|
6
13
|
|
|
@@ -9,10 +16,31 @@ class ValidationSet:
|
|
|
9
16
|
def __init__(
|
|
10
17
|
self,
|
|
11
18
|
validators: list[Validator],
|
|
19
|
+
flags: list[str] | None = None,
|
|
20
|
+
attributes: dict[str, Any] | None = None,
|
|
21
|
+
availability_zones: list[str] | None = None,
|
|
22
|
+
import_values: dict[str, tuple[str, CommentedMap]] | None = None,
|
|
23
|
+
mappings: dict[str, str] | None = None,
|
|
24
|
+
parameters: dict[str, Any] | None = None,
|
|
25
|
+
references: dict[str, str] | None = None,
|
|
12
26
|
):
|
|
13
|
-
|
|
27
|
+
|
|
28
|
+
if flags is None:
|
|
29
|
+
flags = []
|
|
30
|
+
|
|
31
|
+
self._evaluator = Evaluator(flags=flags)
|
|
14
32
|
self._validators = validators
|
|
15
33
|
|
|
34
|
+
self._attributes: dict[str, str] | None = attributes
|
|
35
|
+
self._availability_zones: list[str] | None = availability_zones
|
|
36
|
+
self._mappings: dict[str, str] | None = mappings
|
|
37
|
+
self._import_values: dict[
|
|
38
|
+
str,
|
|
39
|
+
CommentedMap | CommentedSeq | TaggedScalar | YamlObject,
|
|
40
|
+
] | None = import_values
|
|
41
|
+
self._parameters: dict[str, str] | None = parameters
|
|
42
|
+
self._references: dict[str, str] | None = references
|
|
43
|
+
|
|
16
44
|
@property
|
|
17
45
|
def count(self):
|
|
18
46
|
return len(self._validators)
|
|
@@ -44,7 +72,10 @@ class ValidationSet:
|
|
|
44
72
|
validator: Validator,
|
|
45
73
|
template: str,
|
|
46
74
|
):
|
|
47
|
-
found = self._evaluator.match(
|
|
75
|
+
found = self._evaluator.match(
|
|
76
|
+
template,
|
|
77
|
+
validator.query,
|
|
78
|
+
)
|
|
48
79
|
|
|
49
80
|
assert len(found) > 0, f"❌ No results matching results for query {validator.query}"
|
|
50
81
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .renderer import Renderer as Renderer
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class IPv4CIDRSolver:
|
|
2
|
+
|
|
3
|
+
def __init__(
|
|
4
|
+
self,
|
|
5
|
+
host: str,
|
|
6
|
+
desired: int,
|
|
7
|
+
bits: int,
|
|
8
|
+
):
|
|
9
|
+
self.host = host
|
|
10
|
+
self.subnets_desired = desired
|
|
11
|
+
self.subnet_bits = bits
|
|
12
|
+
|
|
13
|
+
host_ip, mask = self.host.split('/', maxsplit=1)
|
|
14
|
+
|
|
15
|
+
self.host_ip = host_ip
|
|
16
|
+
self._host_mask_string = f'/{mask}'
|
|
17
|
+
self.host_mask = int(mask)
|
|
18
|
+
|
|
19
|
+
self.subnet_mask = 32 - bits
|
|
20
|
+
|
|
21
|
+
self._host_octets = [
|
|
22
|
+
int(octet) for octet in self.host.strip(self._host_mask_string).split('.')
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def provision_subnets(self):
|
|
26
|
+
subnet_requested_ips = 2**self.subnet_bits
|
|
27
|
+
host_available_ips = 2**(32 - self.host_mask)
|
|
28
|
+
|
|
29
|
+
total_ips_requested = subnet_requested_ips * self.subnets_desired
|
|
30
|
+
if host_available_ips < total_ips_requested:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
self._provision_subnet(
|
|
35
|
+
subnet_requested_ips,
|
|
36
|
+
idx
|
|
37
|
+
) for idx in range(self.subnets_desired)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _provision_subnet(
|
|
42
|
+
self,
|
|
43
|
+
requested_ips: int,
|
|
44
|
+
idx: int,
|
|
45
|
+
):
|
|
46
|
+
increment = requested_ips
|
|
47
|
+
octet_idx = -1
|
|
48
|
+
if requested_ips > 255:
|
|
49
|
+
increment /= 256
|
|
50
|
+
octet_idx -= 1
|
|
51
|
+
|
|
52
|
+
increment *= idx
|
|
53
|
+
|
|
54
|
+
subnet = list(self._host_octets)
|
|
55
|
+
|
|
56
|
+
subnet[octet_idx] += increment
|
|
57
|
+
|
|
58
|
+
subnet_base_ip = '.'.join([
|
|
59
|
+
str(octet) for octet in subnet
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
return f'{subnet_base_ip}/{self.subnet_mask}'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|