qontract-reconcile 0.10.1rc558__py3-none-any.whl → 0.10.1rc560__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.1rc558
3
+ Version: 0.10.1rc560
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
@@ -9,7 +9,7 @@ reconcile/aws_iam_password_reset.py,sha256=NwErtrqgBiXr7eGCAHdtGGOx0S7-4JnSc29Ie
9
9
  reconcile/aws_support_cases_sos.py,sha256=Jk6_XjDeJSYxgRGqcEAOcynt9qJF2r5HPIPcSKmoBv8,2974
10
10
  reconcile/blackbox_exporter_endpoint_monitoring.py,sha256=W_VJagnsJR1v5oqjlI3RJJE0_nhtJ0m81RS8zWA5u5c,3538
11
11
  reconcile/checkpoint.py,sha256=R2WFXUXLTB4sWMi4GeA4eegsuf_1-Q4vH8M0Toh3Ij4,5036
12
- reconcile/cli.py,sha256=R8qe7Wn89TUlbVhW1a5A-dwu2o-ekIF_o4mdv9Yfz3I,88594
12
+ reconcile/cli.py,sha256=mHp_Jym5aUlvRmijHeL32bEIpx1gzvilHurYyS7WUOs,88893
13
13
  reconcile/closedbox_endpoint_monitoring_base.py,sha256=SMhkcQqprWvThrIJa3U_3uh5w1h-alleW1QnCJFY4Qw,4909
14
14
  reconcile/cluster_deployment_mapper.py,sha256=2Ah-nu-Mdig0pjuiZl_XLrmVAjYzFjORR3dMlCgkmw0,2352
15
15
  reconcile/dashdotdb_base.py,sha256=a5aPLVxyqPSbjdB0Ty-uliOtxwvEbbEljHJKxdK3-Zk,4813
@@ -300,6 +300,8 @@ reconcile/gql_definitions/status_board/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
300
300
  reconcile/gql_definitions/status_board/status_board.py,sha256=vHEzncabujkqbjJ-ibMYNJTODgTc4DMf4y6TW3I_7II,4700
301
301
  reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
302
302
  reconcile/gql_definitions/statuspage/statuspages.py,sha256=gxDb42H93nwtBg7oFRb6Gk9pbAZpsWk_y4Y0s3_g3nE,3520
303
+ reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
+ reconcile/gql_definitions/templating/templates.py,sha256=ujPPFZm9BbkRrxkcR7ZyReDYfFem4uxONYoPuzgiBTc,2735
303
305
  reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
306
  reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py,sha256=eyGX9HcTF6MZbOYZ6Kl6Mg3k6nJTUtwqs9gDxBP_8Dk,1920
305
307
  reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py,sha256=uVZYu5EUcvdAQYBK5YKD0mjoMKDb5inSuCJrrOD5KpE,5704
@@ -382,6 +384,9 @@ reconcile/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
382
384
  reconcile/templates/aws_access_key_email.j2,sha256=2MUr1ERmyISzKgHqsWYLd-1Wbl-peUa-FsGUS-JLUFc,238
383
385
  reconcile/templates/email.yml.j2,sha256=OZgczNRgXPj2gVYTgwQyHAQrMGu7xp-e4W1rX19GcrU,690
384
386
  reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9igCUQhCnFDYh6xw-zcIbU,570
387
+ reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
388
+ reconcile/templating/rendering.py,sha256=3kHB9rBzndWLqW36LWyqCo0ysV4_xdAucsJW1vnwV2A,3904
389
+ reconcile/templating/validator.py,sha256=0bWxJOQL5UQ9iQT6XAnQuSIS5MJ7uE1Yf8yZJVy-EHo,3381
385
390
  reconcile/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
386
391
  reconcile/test/conftest.py,sha256=rQousYrxUz-EwAIbsYO6bIwR1B4CrOz9y_zaUVo2lfI,4466
387
392
  reconcile/test/fixtures.py,sha256=9SDWAUlSd1rCx7z3GhULHcpr-I6FyCsXxaFAZIqYQsQ,591
@@ -651,7 +656,7 @@ reconcile/utils/runtime/sharding.py,sha256=roCdbnBklhTK_g34zbgQYqzpKPaNQ8J6Xd9XL
651
656
  reconcile/utils/saasherder/__init__.py,sha256=J3MBZBFa5YmhqYm08QsjBXz8mFcVOCiOCkyIcw41t7E,343
652
657
  reconcile/utils/saasherder/interfaces.py,sha256=XXY35h8VWQ66z3LBPxaoUAMkIW50264DQiecrzyV6oA,9076
653
658
  reconcile/utils/saasherder/models.py,sha256=PBv8DuAb6KUw_ayn5Ufiya20cCAelBv6Iv--x7hbpa4,5449
654
- reconcile/utils/saasherder/saasherder.py,sha256=HuJQWnugPFWIVMPpH-5Hw4KLhRY3OVlCMGED74Pu9Xc,85737
659
+ reconcile/utils/saasherder/saasherder.py,sha256=vZ0S_7cT2FP1al2zOwLNuOedx8M7MIvokXiCsdm-5W4,85739
655
660
  reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
656
661
  reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
657
662
  reconcile/utils/terraform/config_client.py,sha256=py-Ree-QUYD6Hvng6bM40VgSuttteehIKNgwOSoJO1o,4706
@@ -679,8 +684,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
679
684
  tools/test/test_qontract_cli.py,sha256=se-YG_YVCWRFrnCPvBVHDBT_59CkbIoEni-4SJa8_MU,2755
680
685
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
681
686
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
682
- qontract_reconcile-0.10.1rc558.dist-info/METADATA,sha256=zuvGj1oR22yLBw_2XFSDHCFjcnnyXTawqr-DjPlqyLY,2349
683
- qontract_reconcile-0.10.1rc558.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
684
- qontract_reconcile-0.10.1rc558.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
685
- qontract_reconcile-0.10.1rc558.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
686
- qontract_reconcile-0.10.1rc558.dist-info/RECORD,,
687
+ qontract_reconcile-0.10.1rc560.dist-info/METADATA,sha256=ehv0dWxZP3LGPKg6XVOI_-eH7A_-cZxdJfuZz3bFTwg,2349
688
+ qontract_reconcile-0.10.1rc560.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
689
+ qontract_reconcile-0.10.1rc560.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
690
+ qontract_reconcile-0.10.1rc560.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
691
+ qontract_reconcile-0.10.1rc560.dist-info/RECORD,,
reconcile/cli.py CHANGED
@@ -1858,6 +1858,17 @@ def terraform_repo(ctx, output_file, gitlab_project_id, gitlab_merge_request_id)
1858
1858
  )
1859
1859
 
1860
1860
 
1861
+ @integration.command(short_help="Test app-interface templates.")
1862
+ @click.pass_context
1863
+ def template_validator(ctx):
1864
+ from reconcile.templating import validator
1865
+
1866
+ run_class_integration(
1867
+ integration=validator.TemplateValidatorIntegration(PydanticRunParams()),
1868
+ ctx=ctx.obj,
1869
+ )
1870
+
1871
+
1861
1872
  @integration.command(short_help="Manage AWS Resources using Terraform.")
1862
1873
  @print_to_file
1863
1874
  @vault_output_path
File without changes
@@ -0,0 +1,94 @@
1
+ """
2
+ Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
3
+ """
4
+ from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
5
+ from datetime import datetime # noqa: F401 # pylint: disable=W0611
6
+ from enum import Enum # noqa: F401 # pylint: disable=W0611
7
+ from typing import ( # noqa: F401 # pylint: disable=W0611
8
+ Any,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from pydantic import ( # noqa: F401 # pylint: disable=W0611
14
+ BaseModel,
15
+ Extra,
16
+ Field,
17
+ Json,
18
+ )
19
+
20
+
21
+ DEFINITION = """
22
+ query Templatev1 {
23
+ template_v1 {
24
+ name
25
+ condition
26
+ patch {
27
+ path
28
+ identifier
29
+ }
30
+ targetPath
31
+ template
32
+ templateTest {
33
+ name
34
+ variables
35
+ current
36
+ expectedOutput
37
+ expectedTargetPath
38
+ expectedToRender
39
+ }
40
+ }
41
+ }
42
+ """
43
+
44
+
45
+ class ConfiguredBaseModel(BaseModel):
46
+ class Config:
47
+ smart_union=True
48
+ extra=Extra.forbid
49
+
50
+
51
+ class TemplatePatchV1(ConfiguredBaseModel):
52
+ path: str = Field(..., alias="path")
53
+ identifier: Optional[str] = Field(..., alias="identifier")
54
+
55
+
56
+ class TemplateTestV1(ConfiguredBaseModel):
57
+ name: str = Field(..., alias="name")
58
+ variables: Optional[Json] = Field(..., alias="variables")
59
+ current: Optional[str] = Field(..., alias="current")
60
+ expected_output: str = Field(..., alias="expectedOutput")
61
+ expected_target_path: Optional[str] = Field(..., alias="expectedTargetPath")
62
+ expected_to_render: Optional[bool] = Field(..., alias="expectedToRender")
63
+
64
+
65
+ class TemplateV1(ConfiguredBaseModel):
66
+ name: str = Field(..., alias="name")
67
+ condition: Optional[str] = Field(..., alias="condition")
68
+ patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
69
+ target_path: str = Field(..., alias="targetPath")
70
+ template: str = Field(..., alias="template")
71
+ template_test: list[TemplateTestV1] = Field(..., alias="templateTest")
72
+
73
+
74
+ class Templatev1QueryData(ConfiguredBaseModel):
75
+ template_v1: Optional[list[TemplateV1]] = Field(..., alias="template_v1")
76
+
77
+
78
+ def query(query_func: Callable, **kwargs: Any) -> Templatev1QueryData:
79
+ """
80
+ This is a convenience function which queries and parses the data into
81
+ concrete types. It should be compatible with most GQL clients.
82
+ You do not have to use it to consume the generated data classes.
83
+ Alternatively, you can also mime and alternate the behavior
84
+ of this function in the caller.
85
+
86
+ Parameters:
87
+ query_func (Callable): Function which queries your GQL Server
88
+ kwargs: optional arguments that will be passed to the query function
89
+
90
+ Returns:
91
+ Templatev1QueryData: queried data parsed into generated classes
92
+ """
93
+ raw_data: dict[Any, Any] = query_func(DEFINITION, **kwargs)
94
+ return Templatev1QueryData(**raw_data)
File without changes
@@ -0,0 +1,113 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Optional
3
+
4
+ from jinja2.sandbox import SandboxedEnvironment
5
+ from pydantic import BaseModel
6
+ from ruamel import yaml
7
+
8
+ from reconcile.gql_definitions.templating.templates import TemplateV1
9
+ from reconcile.utils.jsonpath import parse_jsonpath
10
+
11
+
12
+ class TemplateData(BaseModel):
13
+ variables: dict[str, Any]
14
+ current: Optional[dict[str, Any]]
15
+
16
+
17
+ class Renderer(ABC):
18
+ def __init__(self, template: TemplateV1, data: TemplateData):
19
+ self.template = template
20
+ self.data = data
21
+ self.jinja_env = SandboxedEnvironment()
22
+
23
+ def _jinja2_render_kwargs(self) -> dict[str, Any]:
24
+ return {**self.data.variables, "current": self.data.current}
25
+
26
+ def _render_template(self, template: str) -> str:
27
+ return self.jinja_env.from_string(template).render(
28
+ **self._jinja2_render_kwargs()
29
+ )
30
+
31
+ @abstractmethod
32
+ def render_output(self) -> str:
33
+ """
34
+ Implementation of a renderer is required and should return the entire rendered file as a string.
35
+ """
36
+ pass
37
+
38
+ def render_target_path(self) -> str:
39
+ return self._render_template(self.template.target_path)
40
+
41
+ def render_condition(self) -> bool:
42
+ return self._render_template(self.template.condition or "True") == "True"
43
+
44
+
45
+ class FullRenderer(Renderer):
46
+ def render_output(self) -> str:
47
+ """
48
+ Take the variables from Template Data and render the template with it.
49
+
50
+ This method returns the entire file as a string.
51
+ """
52
+ return self._render_template(self.template.template)
53
+
54
+
55
+ class PatchRenderer(Renderer):
56
+ def render_output(self) -> str:
57
+ """
58
+ Takes the variables from Template Data and render the template with it.
59
+
60
+ This method partially updates the current data with the rendered template.
61
+ It checks the existence of the path in the current data and updates it if it exists.
62
+ If the path is not a list, it will update the value with the rendered template.
63
+
64
+ This method returns the entire file as a string.
65
+ """
66
+ if self.template.patch is None: # here to satisfy mypy
67
+ raise ValueError("PatchRenderer requires a patch")
68
+
69
+ p = parse_jsonpath(self.template.patch.path)
70
+
71
+ matched_values = [match.value for match in p.find(self.data.current)]
72
+
73
+ if len(matched_values) != 1:
74
+ raise ValueError(
75
+ f"Expected exactly one match for {self.template.patch.path}, got {len(matched_values)}"
76
+ )
77
+ matched_value = matched_values[0]
78
+
79
+ data_to_add = yaml.safe_load(self._render_template(self.template.template))
80
+
81
+ if isinstance(matched_value, list):
82
+ if not self.template.patch.identifier:
83
+ raise ValueError(
84
+ f"Expected identifier in patch for list at {self.template}"
85
+ )
86
+ dta_identifier = data_to_add.get(self.template.patch.identifier)
87
+ if not dta_identifier:
88
+ raise ValueError(
89
+ f"Expected identifier {self.template.patch.identifier} in data to add"
90
+ )
91
+
92
+ data = next(
93
+ (
94
+ data
95
+ for data in matched_value
96
+ if data.get(self.template.patch.identifier) == dta_identifier
97
+ ),
98
+ None,
99
+ )
100
+ if data is None:
101
+ matched_value.append(data_to_add)
102
+ else:
103
+ data.update(data_to_add)
104
+ else:
105
+ matched_value.update(data_to_add)
106
+
107
+ return yaml.dump(self.data.current, width=4096, Dumper=yaml.RoundTripDumper)
108
+
109
+
110
+ def create_renderer(template: TemplateV1, data: TemplateData) -> Renderer:
111
+ if template.patch:
112
+ return PatchRenderer(template=template, data=data)
113
+ return FullRenderer(template=template, data=data)
@@ -0,0 +1,101 @@
1
+ import logging
2
+ from difflib import context_diff
3
+ from typing import Callable, Optional
4
+
5
+ from pydantic import BaseModel
6
+ from ruamel import yaml
7
+
8
+ from reconcile.gql_definitions.templating.templates import TemplateV1, query
9
+ from reconcile.templating.rendering import TemplateData, create_renderer
10
+ from reconcile.utils import gql
11
+ from reconcile.utils.runtime.integration import (
12
+ QontractReconcileIntegration,
13
+ RunParamsTypeVar,
14
+ )
15
+
16
+ QONTRACT_INTEGRATION = "template-validator"
17
+
18
+
19
+ def get_templates(
20
+ query_func: Optional[Callable] = None,
21
+ ) -> list[TemplateV1]:
22
+ if not query_func:
23
+ query_func = gql.get_api().query
24
+ return query(query_func).template_v1 or []
25
+
26
+
27
+ class TemplateDiff(BaseModel):
28
+ template: str
29
+ test: str
30
+ diff: str
31
+
32
+
33
+ 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
+ )
45
+ )
46
+ if diff:
47
+ self.diffs.append(
48
+ TemplateDiff(template=template_name, test=test_name, diff="".join(diff))
49
+ )
50
+
51
+ def run(self, dry_run: bool) -> None:
52
+ for template in get_templates():
53
+ for test in template.template_test:
54
+ logging.info(f"Running test {test.name} for template {template.name}")
55
+
56
+ r = create_renderer(
57
+ template,
58
+ TemplateData(
59
+ variables=test.variables or {},
60
+ current=yaml.load(
61
+ test.current or "", Loader=yaml.RoundTripLoader
62
+ ),
63
+ ),
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
+ )
91
+
92
+ if self.diffs:
93
+ for diff in self.diffs:
94
+ logging.error(
95
+ f"template: {diff.template}, test: {diff.test}: {diff.diff}"
96
+ )
97
+ raise ValueError("Template validation")
98
+
99
+ @property
100
+ def name(self) -> str:
101
+ return QONTRACT_INTEGRATION
@@ -147,7 +147,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
147
147
  self.state = state
148
148
  self._promotion_state = PromotionState(state=state) if state else None
149
149
  self._channel_map = self._assemble_channels(saas_files=all_saas_files)
150
- self.images: list[str] = []
150
+ self.images: set[str] = set()
151
151
 
152
152
  # each namespace is in fact a target,
153
153
  # so we can use it to calculate.
@@ -1172,7 +1172,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1172
1172
  self._collect_images, resources, self.available_thread_pool_size
1173
1173
  )
1174
1174
  images = set(itertools.chain.from_iterable(images_list))
1175
- self.images.extend(images)
1175
+ self.images.update(images)
1176
1176
  if not images:
1177
1177
  return False # no errors
1178
1178
  errors = threaded.run(