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.
- {qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/RECORD +12 -7
- reconcile/cli.py +11 -0
- reconcile/gql_definitions/templating/__init__.py +0 -0
- reconcile/gql_definitions/templating/templates.py +94 -0
- reconcile/templating/__init__.py +0 -0
- reconcile/templating/rendering.py +113 -0
- reconcile/templating/validator.py +101 -0
- reconcile/utils/saasherder/saasherder.py +2 -2
- {qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.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.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
|
{qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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.
|
683
|
-
qontract_reconcile-0.10.
|
684
|
-
qontract_reconcile-0.10.
|
685
|
-
qontract_reconcile-0.10.
|
686
|
-
qontract_reconcile-0.10.
|
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:
|
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.
|
1175
|
+
self.images.update(images)
|
1176
1176
|
if not images:
|
1177
1177
|
return False # no errors
|
1178
1178
|
errors = threaded.run(
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc558.dist-info → qontract_reconcile-0.10.1rc560.dist-info}/top_level.txt
RENAMED
File without changes
|