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.

@@ -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)
@@ -2,22 +2,24 @@
2
2
  import asyncio
3
3
  import os
4
4
  import pathlib
5
- import yaml
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
- def open_template(path: str) -> YamlObject | None:
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 f:
20
- return yaml.load(f, Loader=Loader)
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
- for tag in tags:
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(templates):
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
- pass
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
- items.append(resources)
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