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.
- {qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/METADATA +2 -1
- {qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/RECORD +8 -7
- {qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/entry_points.txt +1 -0
- reconcile/templating/rendering.py +11 -5
- reconcile/templating/validator.py +85 -56
- tools/template_validation.py +107 -0
- {qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/METADATA
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: qontract-reconcile
|
3
|
-
Version: 0.10.
|
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
|
|
{qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/RECORD
RENAMED
@@ -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=
|
397
|
-
reconcile/templating/validator.py,sha256=
|
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.
|
713
|
-
qontract_reconcile-0.10.
|
714
|
-
qontract_reconcile-0.10.
|
715
|
-
qontract_reconcile-0.10.
|
716
|
-
qontract_reconcile-0.10.
|
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 =
|
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
|
-
|
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
|
9
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
93
|
-
for diff in
|
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
|
File without changes
|
{qontract_reconcile-0.10.1rc606.dist-info → qontract_reconcile-0.10.1rc607.dist-info}/top_level.txt
RENAMED
File without changes
|