qontract-reconcile 0.10.1rc606__py3-none-any.whl → 0.10.1rc607__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc606
3
+ Version: 0.10.1rc607
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -62,4 +62,5 @@ Requires-Dist: requests-oauthlib ~=1.3
62
62
  Requires-Dist: dt ==1.1.61
63
63
  Requires-Dist: jsonpatch ~=1.33
64
64
  Requires-Dist: jsonpointer ~=2.4
65
+ Requires-Dist: yamllint ==1.34.0
65
66
 
@@ -393,8 +393,8 @@ reconcile/templates/email.yml.j2,sha256=OZgczNRgXPj2gVYTgwQyHAQrMGu7xp-e4W1rX19G
393
393
  reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9igCUQhCnFDYh6xw-zcIbU,570
394
394
  reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
395
395
  reconcile/templating/renderer.py,sha256=IOZN0ASSKqp4JGXZ1BDba4XosD3nCuqhStxHMGBcxDI,5119
396
- reconcile/templating/rendering.py,sha256=2kMfstQz12FL-Qfh9OBuzAcYY1OggbFQrCYx1nxERSg,4737
397
- reconcile/templating/validator.py,sha256=W64nR1eoKVDGewJ4DEX9ZiWWKjFEKg5w5Xi6lrrPBd8,3434
396
+ reconcile/templating/rendering.py,sha256=msMGqCNh7M0c6A1l_7KBmT3GRHH9gsB8htN_9bGtwe4,4949
397
+ reconcile/templating/validator.py,sha256=6ZZs5cREIndgAUHiR1SeDGwaeSLJTJBKHww0nXzgqhQ,4141
398
398
  reconcile/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
399
399
  reconcile/test/conftest.py,sha256=rQousYrxUz-EwAIbsYO6bIwR1B4CrOz9y_zaUVo2lfI,4466
400
400
  reconcile/test/fixtures.py,sha256=9SDWAUlSd1rCx7z3GhULHcpr-I6FyCsXxaFAZIqYQsQ,591
@@ -700,6 +700,7 @@ tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QE
700
700
  tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
701
701
  tools/qontract_cli.py,sha256=cnZLJJS-FRUQFUAD90ivt-WEv8NfaemqBvZ9CaFV_kI,110957
702
702
  tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
703
+ tools/template_validation.py,sha256=XZ6i1iTN2Lknh-8uS-kcbTmYBzJhzl2tLYN6hP33N2Q,3244
703
704
  tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
704
705
  tools/cli_commands/gpg_encrypt.py,sha256=w8hl4jIEWk5wKbEFN6fVEOwUJGmdlvOqYodW3XSN7mU,4978
705
706
  tools/sre_checkpoints/__init__.py,sha256=CDaDaywJnmRCLyl_NCcvxi-Zc0hTi_3OdwKiFOyS39I,145
@@ -709,8 +710,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
709
710
  tools/test/test_qontract_cli.py,sha256=OvalpVRfY4pNmpMaWHHYqBjV68b1eGQjX8SCyTAXb1w,3501
710
711
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
711
712
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
712
- qontract_reconcile-0.10.1rc606.dist-info/METADATA,sha256=eUhofv8hXCCHrIOqQ_i7gu9ICj-Yd6JG8k3SibY-ZlY,2349
713
- qontract_reconcile-0.10.1rc606.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
714
- qontract_reconcile-0.10.1rc606.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
715
- qontract_reconcile-0.10.1rc606.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
716
- qontract_reconcile-0.10.1rc606.dist-info/RECORD,,
713
+ qontract_reconcile-0.10.1rc607.dist-info/METADATA,sha256=YA_uMko_z5JCYKe6FRbUNu7lVLlorRq_4HKpwJDrj3Q,2382
714
+ qontract_reconcile-0.10.1rc607.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
715
+ qontract_reconcile-0.10.1rc607.dist-info/entry_points.txt,sha256=rIxI5zWtHNlfpDeq1a7pZXAPoqf7HG32KMTN3MeWK_8,429
716
+ qontract_reconcile-0.10.1rc607.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
717
+ qontract_reconcile-0.10.1rc607.dist-info/RECORD,,
@@ -5,3 +5,4 @@ glitchtip-access-reporter = tools.glitchtip_access_reporter:main
5
5
  glitchtip-access-revalidation = tools.glitchtip_access_revalidation:main
6
6
  qontract-cli = tools.qontract_cli:root
7
7
  qontract-reconcile = reconcile.cli:integration
8
+ template-validation = tools.template_validation:main
@@ -1,12 +1,13 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
+ from io import StringIO
3
4
  from typing import Any, Optional, Protocol
4
5
 
5
6
  from pydantic import BaseModel
6
- from ruamel import yaml
7
7
 
8
8
  from reconcile.utils.jinja2.utils import Jinja2TemplateError, process_jinja2_template
9
9
  from reconcile.utils.jsonpath import parse_jsonpath
10
+ from reconcile.utils.ruamel import create_ruamel_instance
10
11
  from reconcile.utils.secret_reader import SecretReaderBase
11
12
 
12
13
 
@@ -40,11 +41,12 @@ class Renderer(ABC):
40
41
  self,
41
42
  template: Template,
42
43
  data: TemplateData,
43
- secret_reader: SecretReaderBase,
44
+ secret_reader: Optional[SecretReaderBase] = None,
44
45
  ):
45
46
  self.template = template
46
47
  self.data = data
47
48
  self.secret_reader = secret_reader
49
+ self.ruaml_instace = create_ruamel_instance()
48
50
 
49
51
  def _jinja2_render_kwargs(self) -> dict[str, Any]:
50
52
  return {**self.data.variables, "current": self.data.current}
@@ -108,7 +110,9 @@ class PatchRenderer(Renderer):
108
110
  )
109
111
  matched_value = matched_values[0]
110
112
 
111
- data_to_add = yaml.safe_load(self._render_template(self.template.template))
113
+ data_to_add = self.ruaml_instace.load(
114
+ self._render_template(self.template.template)
115
+ )
112
116
 
113
117
  if isinstance(matched_value, list):
114
118
  if not self.template.patch.identifier:
@@ -136,13 +140,15 @@ class PatchRenderer(Renderer):
136
140
  else:
137
141
  matched_value.update(data_to_add)
138
142
 
139
- return yaml.dump(self.data.current, width=4096, Dumper=yaml.RoundTripDumper)
143
+ with StringIO() as s:
144
+ self.ruaml_instace.dump(self.data.current, s)
145
+ return s.getvalue()
140
146
 
141
147
 
142
148
  def create_renderer(
143
149
  template: Template,
144
150
  data: TemplateData,
145
- secret_reader: SecretReaderBase,
151
+ secret_reader: Optional[SecretReaderBase] = None,
146
152
  ) -> Renderer:
147
153
  if template.patch:
148
154
  return PatchRenderer(template=template, data=data, secret_reader=secret_reader)
@@ -5,13 +5,17 @@ from typing import Callable, Optional
5
5
  from pydantic import BaseModel
6
6
  from ruamel import yaml
7
7
 
8
- from reconcile.gql_definitions.templating.templates import TemplateV1, query
9
- from reconcile.templating.rendering import TemplateData, create_renderer
8
+ from reconcile.gql_definitions.templating.templates import (
9
+ TemplateTestV1,
10
+ TemplateV1,
11
+ query,
12
+ )
13
+ from reconcile.templating.rendering import Renderer, TemplateData, create_renderer
10
14
  from reconcile.utils import gql
11
15
  from reconcile.utils.runtime.integration import (
12
16
  QontractReconcileIntegration,
13
- RunParamsTypeVar,
14
17
  )
18
+ from reconcile.utils.secret_reader import SecretReaderBase
15
19
 
16
20
  QONTRACT_INTEGRATION = "template-validator"
17
21
 
@@ -31,70 +35,95 @@ class TemplateDiff(BaseModel):
31
35
 
32
36
 
33
37
  class TemplateValidatorIntegration(QontractReconcileIntegration):
34
- def __init__(self, params: RunParamsTypeVar) -> None:
35
- super().__init__(params)
36
- self.diffs: list[TemplateDiff] = []
37
-
38
- def diff_result(
39
- self, template_name: str, test_name: str, output: str, expected: str
40
- ) -> None:
41
- diff = list(
42
- context_diff(
43
- output.splitlines(keepends=True), expected.splitlines(keepends=True)
44
- )
38
+ @staticmethod
39
+ def _create_renderer(
40
+ template: TemplateV1,
41
+ template_test: TemplateTestV1,
42
+ secret_reader: Optional[SecretReaderBase] = None,
43
+ ) -> Renderer:
44
+ return create_renderer(
45
+ template,
46
+ TemplateData(
47
+ variables=template_test.variables or {},
48
+ current=yaml.load(
49
+ template_test.current or "", Loader=yaml.RoundTripLoader
50
+ ),
51
+ ),
52
+ secret_reader=secret_reader,
53
+ )
54
+
55
+ @staticmethod
56
+ def validate_template(
57
+ template: TemplateV1,
58
+ template_test: TemplateTestV1,
59
+ secret_reader: Optional[SecretReaderBase] = None,
60
+ ) -> list[TemplateDiff]:
61
+ diffs: list[TemplateDiff] = []
62
+
63
+ r = TemplateValidatorIntegration._create_renderer(
64
+ template, template_test, secret_reader=secret_reader
45
65
  )
46
- if diff:
47
- self.diffs.append(
48
- TemplateDiff(template=template_name, test=test_name, diff="".join(diff))
66
+
67
+ # Check target path
68
+ if template_test.expected_target_path:
69
+ rendered_target_path = r.render_target_path().strip()
70
+ if rendered_target_path != template_test.expected_target_path.strip():
71
+ diffs.append(
72
+ TemplateDiff(
73
+ template=template.name,
74
+ test=template_test.name,
75
+ diff=f"Target path mismatch, got: {rendered_target_path}, expected: {template_test.expected_target_path}",
76
+ )
77
+ )
78
+
79
+ # Check condition
80
+ should_render = r.render_condition()
81
+ if (
82
+ template_test.expected_to_render is not None
83
+ and template_test.expected_to_render != should_render
84
+ ):
85
+ diffs.append(
86
+ TemplateDiff(
87
+ template=template.name,
88
+ test=template_test.name,
89
+ diff=f"Condition mismatch, got: {should_render}, expected: {template_test.expected_to_render}",
90
+ )
49
91
  )
50
92
 
93
+ # Check template output
94
+ if should_render:
95
+ output = r.render_output()
96
+ diff = list(
97
+ context_diff(
98
+ output.splitlines(keepends=True),
99
+ template_test.expected_output.splitlines(keepends=True),
100
+ )
101
+ )
102
+ if diff:
103
+ diffs.append(
104
+ TemplateDiff(
105
+ template=template.name,
106
+ test=template_test.name,
107
+ diff="".join(diff),
108
+ )
109
+ )
110
+
111
+ return diffs
112
+
51
113
  def run(self, dry_run: bool) -> None:
114
+ diffs: list[TemplateDiff] = []
115
+
52
116
  for template in get_templates():
53
117
  for test in template.template_test:
54
118
  logging.info(f"Running test {test.name} for template {template.name}")
55
- r = create_renderer(
56
- template,
57
- TemplateData(
58
- variables=test.variables or {},
59
- current=yaml.load(
60
- test.current or "", Loader=yaml.RoundTripLoader
61
- ),
62
- ),
63
- secret_reader=self.secret_reader,
64
- )
65
- if test.expected_target_path:
66
- self.diff_result(
67
- template.name,
68
- test.name,
69
- r.render_target_path().strip(),
70
- test.expected_target_path.strip(),
71
- )
72
- should_render = r.render_condition()
73
- if (
74
- test.expected_to_render is not None
75
- and test.expected_to_render != should_render
76
- ):
77
- self.diffs.append(
78
- TemplateDiff(
79
- template=template.name,
80
- test=test.name,
81
- diff=f"Condition mismatch, got: {should_render}, expected: {test.expected_to_render}",
82
- )
83
- )
84
- if should_render:
85
- self.diff_result(
86
- template.name,
87
- test.name,
88
- r.render_output().strip(),
89
- test.expected_output.strip(),
90
- )
119
+ diffs.extend(self.validate_template(template, test, self.secret_reader))
91
120
 
92
- if self.diffs:
93
- for diff in self.diffs:
121
+ if diffs:
122
+ for diff in diffs:
94
123
  logging.error(
95
124
  f"template: {diff.template}, test: {diff.test}: {diff.diff}"
96
125
  )
97
- raise ValueError("Template validation")
126
+ raise ValueError("Template validation failed")
98
127
 
99
128
  @property
100
129
  def name(self) -> str:
@@ -0,0 +1,107 @@
1
+ import json
2
+ import os.path
3
+ import sys
4
+
5
+ import click
6
+ from yamllint import linter # type: ignore
7
+ from yamllint.config import YamlLintConfig # type: ignore
8
+
9
+ from reconcile.gql_definitions.templating.templates import TemplateV1
10
+ from reconcile.templating.validator import TemplateDiff, TemplateValidatorIntegration
11
+ from reconcile.utils.models import data_default_none
12
+ from reconcile.utils.ruamel import create_ruamel_instance
13
+
14
+
15
+ def load_clean_yaml(ai_path: str, path: str) -> dict:
16
+ if not path.startswith("/data/"):
17
+ to_load = os.path.join(ai_path, "data", path.lstrip("/"))
18
+ else:
19
+ to_load = os.path.join(ai_path, path.lstrip("/"))
20
+
21
+ y = load_yaml(to_load)
22
+
23
+ if "$schema" in y:
24
+ del y["$schema"]
25
+ return y
26
+
27
+
28
+ def load_yaml(to_load: str) -> dict:
29
+ ruamel_instance = create_ruamel_instance()
30
+ with open(to_load, "r", encoding="utf-8") as file:
31
+ return ruamel_instance.load(file)
32
+
33
+
34
+ def print_lint_problems(lint_problems: list[linter.LintProblem]) -> None:
35
+ print("--------- LINTING ISSUES ---------")
36
+ print(f"Found {len(lint_problems)} lint problems:")
37
+ for problem in lint_problems:
38
+ print(f"Lint error in line: {problem.line}, {problem.desc}")
39
+ print("--------- END LINTING ISSUES ---------")
40
+
41
+
42
+ def print_test_diffs(diffs: list[TemplateDiff]) -> None:
43
+ print("--------- TEMPLATE TEST DIFF ---------")
44
+ print("Template validation failed")
45
+ for d in diffs:
46
+ for line in d.diff.splitlines():
47
+ if line:
48
+ print(f"{line}")
49
+ print("--------- END TEMPLATE TEST DIFF ---------")
50
+
51
+
52
+ @click.command()
53
+ @click.option(
54
+ "--ai-path",
55
+ help="Path to the bundle file",
56
+ default=None,
57
+ required=True,
58
+ )
59
+ @click.option(
60
+ "--template-path",
61
+ help="Path to the template file",
62
+ default=None,
63
+ required=True,
64
+ )
65
+ def main(ai_path: str, template_path: str) -> None:
66
+ okay = True
67
+ templateRaw = load_clean_yaml(ai_path, template_path)
68
+
69
+ tests = []
70
+ for testRaw in templateRaw["templateTest"]:
71
+ test_yaml = load_clean_yaml(ai_path, testRaw["$ref"])
72
+ variables = json.dumps(test_yaml["variables"])
73
+ test_yaml["variables"] = variables
74
+ tests.append(test_yaml)
75
+
76
+ templateRaw["templateTest"] = tests
77
+ template: TemplateV1 = TemplateV1(**data_default_none(TemplateV1, templateRaw))
78
+
79
+ # templates_to_validate = {}
80
+ for test in template.template_test:
81
+ diffs: list[TemplateDiff] = []
82
+ print("Running tests:", test.name)
83
+ diffs.extend(TemplateValidatorIntegration.validate_template(template, test))
84
+
85
+ renderer = TemplateValidatorIntegration._create_renderer(template, test)
86
+ if renderer.render_condition():
87
+ output = renderer.render_output()
88
+ lint_problems = list(
89
+ linter.run(output, YamlLintConfig(file=f"{ai_path}/.yamllint"), "")
90
+ )
91
+ if lint_problems:
92
+ okay = False
93
+ print_lint_problems(lint_problems)
94
+
95
+ if diffs:
96
+ okay = False
97
+ print_test_diffs(diffs)
98
+
99
+ if okay:
100
+ print("... passed")
101
+ sys.exit(0)
102
+ print("... failed")
103
+ sys.exit(1)
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main() # pylint: disable=no-value-for-parameter