qontract-reconcile 0.10.1rc922__py3-none-any.whl → 0.10.1rc924__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.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/RECORD +14 -14
- reconcile/gql_definitions/templating/template_collection.py +12 -0
- reconcile/gql_definitions/templating/templates.py +12 -0
- reconcile/templating/lib/rendering.py +16 -2
- reconcile/templating/renderer.py +30 -7
- reconcile/templating/validator.py +12 -0
- reconcile/test/test_saasherder.py +61 -0
- reconcile/utils/jinja2/utils.py +39 -7
- reconcile/utils/promotion_state.py +5 -0
- reconcile/utils/saasherder/saasherder.py +15 -1
- {qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.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.1rc924
|
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.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/RECORD
RENAMED
@@ -366,8 +366,8 @@ reconcile/gql_definitions/status_board/status_board.py,sha256=vHEzncabujkqbjJ-ib
|
|
366
366
|
reconcile/gql_definitions/statuspage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
367
367
|
reconcile/gql_definitions/statuspage/statuspages.py,sha256=CTRzjiR9k41LqlkgyoNHwC2JERsoD_Run_aK7jw_Ono,5299
|
368
368
|
reconcile/gql_definitions/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
369
|
-
reconcile/gql_definitions/templating/template_collection.py,sha256=
|
370
|
-
reconcile/gql_definitions/templating/templates.py,sha256=
|
369
|
+
reconcile/gql_definitions/templating/template_collection.py,sha256=QA68QNszPgJY996pPPJmdTULI1gjcT8MDvk1NLKi6_Y,4017
|
370
|
+
reconcile/gql_definitions/templating/templates.py,sha256=bV1JWwxDW8opARBOrZ_h5NBsJfM-9aL-povmTZnpTOk,3296
|
371
371
|
reconcile/gql_definitions/terraform_cloudflare_dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
372
372
|
reconcile/gql_definitions/terraform_cloudflare_dns/app_interface_cloudflare_dns_settings.py,sha256=eyGX9HcTF6MZbOYZ6Kl6Mg3k6nJTUtwqs9gDxBP_8Dk,1920
|
373
373
|
reconcile/gql_definitions/terraform_cloudflare_dns/terraform_cloudflare_zones.py,sha256=dBQ2tyAp-eRZs59mguaTc6-x67JUoSxtZ8mOjbRqDuc,5832
|
@@ -463,12 +463,12 @@ reconcile/templates/jira-checkpoint-missinginfo.j2,sha256=c_Vvg-lEENsB3tgxm9B6Y9
|
|
463
463
|
reconcile/templates/rosa-classic-cluster-creation.sh.j2,sha256=FVBmnR2FmVModhqOYNBInhF8zk0Qnj9og9KHK5-X9v0,2361
|
464
464
|
reconcile/templates/rosa-hcp-cluster-creation.sh.j2,sha256=2wTdv9qvapCvT8NSi_hq8sXhpFSaxRX-V6Cao1diCI8,2393
|
465
465
|
reconcile/templating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
466
|
-
reconcile/templating/renderer.py,sha256=
|
467
|
-
reconcile/templating/validator.py,sha256=
|
466
|
+
reconcile/templating/renderer.py,sha256=oUUiHHSgD5bcUN3Xf-yoEezAf3LjqN-2GlYhQUTPIZY,13067
|
467
|
+
reconcile/templating/validator.py,sha256=5f9f35PCHOOdjb7KZquL2YdabyuAUokPDa4xutSEHIQ,5360
|
468
468
|
reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
469
469
|
reconcile/templating/lib/merge_request_manager.py,sha256=4oe3EwQOP7CZSraocivbRzzOuVb0ooElaUS2_DGsF50,5603
|
470
470
|
reconcile/templating/lib/model.py,sha256=fb6FYYLQjmoh2DjVKO7TEWCuDPf1Q34xmOx0M9Z07ek,324
|
471
|
-
reconcile/templating/lib/rendering.py,sha256=
|
471
|
+
reconcile/templating/lib/rendering.py,sha256=6kt8NCCwB4vLKYal7KtRmBDguIC1p_PIQCRr-vL7p5w,5504
|
472
472
|
reconcile/terraform_init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
473
473
|
reconcile/terraform_init/integration.py,sha256=glQ9uy8Kj2aTQXCAupwSFeih7reX_xMX_UuWW_ywBMU,6100
|
474
474
|
reconcile/terraform_init/merge_request.py,sha256=3CYtgSd7Q9zjKg4wsDz437EPCRfGeZZ8fZ0Y-ChKXJY,1475
|
@@ -537,7 +537,7 @@ reconcile/test/test_quay_repos.py,sha256=TdkcRF_a8PLp01Kti9eZZN-vGup2yPBT4Iba3k0
|
|
537
537
|
reconcile/test/test_queries.py,sha256=SpH3RmNpBjEr_ne3VjAMCgKK8RE1z1zo7bypkT5uoO4,1946
|
538
538
|
reconcile/test/test_repo_owners.py,sha256=uRYMLbMmh-9usF0TerabZTZV-Z1CS4I6ybT-LQqCLe8,1423
|
539
539
|
reconcile/test/test_requests_sender.py,sha256=7fd9C2kEFS0-CYtlsif66N1kO9c44pzuBPAJKR9igqU,5385
|
540
|
-
reconcile/test/test_saasherder.py,sha256=
|
540
|
+
reconcile/test/test_saasherder.py,sha256=jUn8d1DtdXq1pfDrThqIwx37Lgxa7mou3aZ-m3UrWdc,57129
|
541
541
|
reconcile/test/test_saasherder_allowed_secret_paths.py,sha256=5NHQwNJO66at6HiyMZ5sVRTQDwxdvlOQo0KmkBWCw5Q,4853
|
542
542
|
reconcile/test/test_secret_reader.py,sha256=kz7nzcPjvA08cytnvcA_PMA98AEyqJWsESkYeRn5xCk,4994
|
543
543
|
reconcile/test/test_slack_base.py,sha256=pTUGvJ2S2wF3PhJyGWmiNXG52QtXKy2cbu-G8Ymrv6I,5019
|
@@ -689,7 +689,7 @@ reconcile/utils/pagerduty_api.py,sha256=_24i9S_4X7nlvHb-7clXRE0p1BG4ODjOzKxWO-F9
|
|
689
689
|
reconcile/utils/parse_dhms_duration.py,sha256=TONpLnec5gHeF7k815YNJpQyDjXhkxZIcv9s8ffbTSY,1840
|
690
690
|
reconcile/utils/password_validator.py,sha256=XwuWg-8CPlcuG7dl_oQ1G1h2gSVSnfMym_VkuprpWVg,2183
|
691
691
|
reconcile/utils/prometheus.py,sha256=Ad0rwLbxRuuYjHwkwJloHEdK0bvy42h-p-HIT1DhDhs,3832
|
692
|
-
reconcile/utils/promotion_state.py,sha256=
|
692
|
+
reconcile/utils/promotion_state.py,sha256=drkR0PSzfsUVPB0pisARzvUfHgk8KPbeK4FUZdP099Y,3894
|
693
693
|
reconcile/utils/promtool.py,sha256=UmBfTHgW9Ys7fZ9BfhIVJEFGLkbge9y1AgL5PNHp7iA,2831
|
694
694
|
reconcile/utils/quay_api.py,sha256=zbwi3YjL7dTDYHGWcrZ0mbxyZQuEB8v3sV_Km2O-mIs,7906
|
695
695
|
reconcile/utils/raw_github_api.py,sha256=O6Q4vq7bi5ZWcfquPutc9rJ4Ef8_sFqd_RLgzpIoj0w,2920
|
@@ -741,7 +741,7 @@ reconcile/utils/internal_groups/models.py,sha256=y_IqBVqfGqNXiu0VudvBWFrm_-uafVm
|
|
741
741
|
reconcile/utils/jinja2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
742
742
|
reconcile/utils/jinja2/extensions.py,sha256=7K-uo6G2eCWa98MHT8fRPYIKCLQB_5D2keqQ_LyAfHM,1293
|
743
743
|
reconcile/utils/jinja2/filters.py,sha256=RVVkpf87FllrPUpqk_8KN-r1IsmnS0bpygAVvsvIr5g,4504
|
744
|
-
reconcile/utils/jinja2/utils.py,sha256=
|
744
|
+
reconcile/utils/jinja2/utils.py,sha256=nyTS7SafbjQliIg0PZhqK0YGfFL0W2V6XA5CRaqOkrA,7809
|
745
745
|
reconcile/utils/jobcontroller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
746
746
|
reconcile/utils/jobcontroller/controller.py,sha256=2V_vm5thFx6adW4bMy9CdHXFesuo6S4lSkEpGxkXSM0,14492
|
747
747
|
reconcile/utils/jobcontroller/models.py,sha256=tSRAkUX23iyn4YPsWEicFXwRxw3mXb5B2pDOWmXX8wQ,6350
|
@@ -793,7 +793,7 @@ reconcile/utils/runtime/sharding.py,sha256=r0ieUtNed7NvknSw6qQrCkKpVXE1shuHGnfFc
|
|
793
793
|
reconcile/utils/saasherder/__init__.py,sha256=J3MBZBFa5YmhqYm08QsjBXz8mFcVOCiOCkyIcw41t7E,343
|
794
794
|
reconcile/utils/saasherder/interfaces.py,sha256=C2wrw34OXypshVocAsPrVZsSHptgw4g9u7Haa2wulZQ,9087
|
795
795
|
reconcile/utils/saasherder/models.py,sha256=6MGie9SqsyP5ySjmk5bO5vPJ0-x53a0uzABxQO-WsB0,9746
|
796
|
-
reconcile/utils/saasherder/saasherder.py,sha256=
|
796
|
+
reconcile/utils/saasherder/saasherder.py,sha256=ZTDf6uIpke6y9jKZgESjdY8hDI6Jf2J2UGZp6LJXBWY,85249
|
797
797
|
reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
|
798
798
|
reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
|
799
799
|
reconcile/utils/terraform/config_client.py,sha256=3gUIIIEv52Vx7-VgQ2FZYfCCrfqUv_5gw_TQ3mbLcTs,4666
|
@@ -844,8 +844,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
|
|
844
844
|
tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
|
845
845
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
846
846
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
847
|
-
qontract_reconcile-0.10.
|
848
|
-
qontract_reconcile-0.10.
|
849
|
-
qontract_reconcile-0.10.
|
850
|
-
qontract_reconcile-0.10.
|
851
|
-
qontract_reconcile-0.10.
|
847
|
+
qontract_reconcile-0.10.1rc924.dist-info/METADATA,sha256=lDNUw1z1rLZBkUJGhvbJutmyOh-8hG0mfjXu2C88-Ck,2273
|
848
|
+
qontract_reconcile-0.10.1rc924.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
849
|
+
qontract_reconcile-0.10.1rc924.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
850
|
+
qontract_reconcile-0.10.1rc924.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
851
|
+
qontract_reconcile-0.10.1rc924.dist-info/RECORD,,
|
@@ -45,6 +45,11 @@ query TemplateCollection_v1 {
|
|
45
45
|
identifier
|
46
46
|
}
|
47
47
|
template
|
48
|
+
templateRenderOptions {
|
49
|
+
trimBlocks
|
50
|
+
lstripBlocks
|
51
|
+
keepTrailingNewline
|
52
|
+
}
|
48
53
|
}
|
49
54
|
}
|
50
55
|
}
|
@@ -76,6 +81,12 @@ class TemplatePatchV1(ConfiguredBaseModel):
|
|
76
81
|
identifier: Optional[str] = Field(..., alias="identifier")
|
77
82
|
|
78
83
|
|
84
|
+
class TemplateRenderOptionsV1(ConfiguredBaseModel):
|
85
|
+
trim_blocks: Optional[bool] = Field(..., alias="trimBlocks")
|
86
|
+
lstrip_blocks: Optional[bool] = Field(..., alias="lstripBlocks")
|
87
|
+
keep_trailing_newline: Optional[bool] = Field(..., alias="keepTrailingNewline")
|
88
|
+
|
89
|
+
|
79
90
|
class TemplateV1(ConfiguredBaseModel):
|
80
91
|
name: str = Field(..., alias="name")
|
81
92
|
auto_approved: Optional[bool] = Field(..., alias="autoApproved")
|
@@ -83,6 +94,7 @@ class TemplateV1(ConfiguredBaseModel):
|
|
83
94
|
target_path: str = Field(..., alias="targetPath")
|
84
95
|
patch: Optional[TemplatePatchV1] = Field(..., alias="patch")
|
85
96
|
template: str = Field(..., alias="template")
|
97
|
+
template_render_options: Optional[TemplateRenderOptionsV1] = Field(..., alias="templateRenderOptions")
|
86
98
|
|
87
99
|
|
88
100
|
class TemplateCollectionV1(ConfiguredBaseModel):
|
@@ -38,6 +38,11 @@ query Templatev1 {
|
|
38
38
|
expectedTargetPath
|
39
39
|
expectedToRender
|
40
40
|
}
|
41
|
+
templateRenderOptions {
|
42
|
+
trimBlocks
|
43
|
+
lstripBlocks
|
44
|
+
keepTrailingNewline
|
45
|
+
}
|
41
46
|
}
|
42
47
|
}
|
43
48
|
"""
|
@@ -63,6 +68,12 @@ class TemplateTestV1(ConfiguredBaseModel):
|
|
63
68
|
expected_to_render: Optional[bool] = Field(..., alias="expectedToRender")
|
64
69
|
|
65
70
|
|
71
|
+
class TemplateRenderOptionsV1(ConfiguredBaseModel):
|
72
|
+
trim_blocks: Optional[bool] = Field(..., alias="trimBlocks")
|
73
|
+
lstrip_blocks: Optional[bool] = Field(..., alias="lstripBlocks")
|
74
|
+
keep_trailing_newline: Optional[bool] = Field(..., alias="keepTrailingNewline")
|
75
|
+
|
76
|
+
|
66
77
|
class TemplateV1(ConfiguredBaseModel):
|
67
78
|
name: str = Field(..., alias="name")
|
68
79
|
auto_approved: Optional[bool] = Field(..., alias="autoApproved")
|
@@ -71,6 +82,7 @@ class TemplateV1(ConfiguredBaseModel):
|
|
71
82
|
target_path: str = Field(..., alias="targetPath")
|
72
83
|
template: str = Field(..., alias="template")
|
73
84
|
template_test: list[TemplateTestV1] = Field(..., alias="templateTest")
|
85
|
+
template_render_options: Optional[TemplateRenderOptionsV1] = Field(..., alias="templateRenderOptions")
|
74
86
|
|
75
87
|
|
76
88
|
class Templatev1QueryData(ConfiguredBaseModel):
|
@@ -5,7 +5,11 @@ from typing import Any, Protocol
|
|
5
5
|
|
6
6
|
from pydantic import BaseModel
|
7
7
|
|
8
|
-
from reconcile.utils.jinja2.utils import
|
8
|
+
from reconcile.utils.jinja2.utils import (
|
9
|
+
Jinja2TemplateError,
|
10
|
+
TemplateRenderOptions,
|
11
|
+
process_jinja2_template,
|
12
|
+
)
|
9
13
|
from reconcile.utils.jsonpath import parse_jsonpath
|
10
14
|
from reconcile.utils.ruamel import create_ruamel_instance
|
11
15
|
from reconcile.utils.secret_reader import SecretReaderBase
|
@@ -43,11 +47,13 @@ class Renderer(ABC):
|
|
43
47
|
template: Template,
|
44
48
|
data: TemplateData,
|
45
49
|
secret_reader: SecretReaderBase | None = None,
|
50
|
+
template_render_options: TemplateRenderOptions | None = None,
|
46
51
|
):
|
47
52
|
self.template = template
|
48
53
|
self.data = data
|
49
54
|
self.secret_reader = secret_reader
|
50
55
|
self.ruamel_instance = create_ruamel_instance(explicit_start=True)
|
56
|
+
self.template_render_options = template_render_options
|
51
57
|
|
52
58
|
def _jinja2_render_kwargs(self) -> dict[str, Any]:
|
53
59
|
return {**self.data.variables, "current": self.data.current}
|
@@ -58,6 +64,7 @@ class Renderer(ABC):
|
|
58
64
|
body=template,
|
59
65
|
vars=self._jinja2_render_kwargs(),
|
60
66
|
secret_reader=self.secret_reader,
|
67
|
+
template_render_options=self.template_render_options,
|
61
68
|
)
|
62
69
|
except Jinja2TemplateError as e:
|
63
70
|
logging.error(f"Error rendering template {self.template.name}: {e}")
|
@@ -150,11 +157,18 @@ def create_renderer(
|
|
150
157
|
template: Template,
|
151
158
|
data: TemplateData,
|
152
159
|
secret_reader: SecretReaderBase | None = None,
|
160
|
+
template_render_options: TemplateRenderOptions | None = None,
|
153
161
|
) -> Renderer:
|
154
162
|
if template.patch:
|
155
|
-
return PatchRenderer(
|
163
|
+
return PatchRenderer(
|
164
|
+
template=template,
|
165
|
+
data=data,
|
166
|
+
secret_reader=secret_reader,
|
167
|
+
template_render_options=template_render_options,
|
168
|
+
)
|
156
169
|
return FullRenderer(
|
157
170
|
template=template,
|
158
171
|
data=data,
|
159
172
|
secret_reader=secret_reader,
|
173
|
+
template_render_options=template_render_options,
|
160
174
|
)
|
reconcile/templating/renderer.py
CHANGED
@@ -23,6 +23,7 @@ from reconcile.templating.lib.merge_request_manager import (
|
|
23
23
|
)
|
24
24
|
from reconcile.templating.lib.model import TemplateInput, TemplateOutput
|
25
25
|
from reconcile.templating.lib.rendering import (
|
26
|
+
Renderer,
|
26
27
|
TemplateData,
|
27
28
|
create_renderer,
|
28
29
|
)
|
@@ -32,12 +33,13 @@ from reconcile.typed_queries.gitlab_instances import get_gitlab_instances
|
|
32
33
|
from reconcile.utils import gql
|
33
34
|
from reconcile.utils.git import clone
|
34
35
|
from reconcile.utils.gql import GqlApi, init_from_config
|
35
|
-
from reconcile.utils.jinja2.utils import process_jinja2_template
|
36
|
+
from reconcile.utils.jinja2.utils import TemplateRenderOptions, process_jinja2_template
|
36
37
|
from reconcile.utils.ruamel import create_ruamel_instance
|
37
38
|
from reconcile.utils.runtime.integration import (
|
38
39
|
PydanticRunParams,
|
39
40
|
QontractReconcileIntegration,
|
40
41
|
)
|
42
|
+
from reconcile.utils.secret_reader import SecretReaderBase
|
41
43
|
from reconcile.utils.vcs import VCS
|
42
44
|
|
43
45
|
QONTRACT_INTEGRATION = "template-renderer"
|
@@ -207,6 +209,31 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
|
|
207
209
|
def __init__(self, params: TemplateRendererIntegrationParams) -> None:
|
208
210
|
super().__init__(params)
|
209
211
|
|
212
|
+
@staticmethod
|
213
|
+
def _create_renderer(
|
214
|
+
template: TemplateV1,
|
215
|
+
variables: dict,
|
216
|
+
secret_reader: SecretReaderBase | None = None,
|
217
|
+
) -> Renderer:
|
218
|
+
return create_renderer(
|
219
|
+
template,
|
220
|
+
TemplateData(
|
221
|
+
variables=variables,
|
222
|
+
),
|
223
|
+
secret_reader=secret_reader,
|
224
|
+
template_render_options=TemplateRenderOptions.create(
|
225
|
+
trim_blocks=template.template_render_options.trim_blocks
|
226
|
+
if template.template_render_options
|
227
|
+
else None,
|
228
|
+
lstrip_blocks=template.template_render_options.lstrip_blocks
|
229
|
+
if template.template_render_options
|
230
|
+
else None,
|
231
|
+
keep_trailing_newline=template.template_render_options.keep_trailing_newline
|
232
|
+
if template.template_render_options
|
233
|
+
else None,
|
234
|
+
),
|
235
|
+
)
|
236
|
+
|
210
237
|
def process_template(
|
211
238
|
self,
|
212
239
|
template: TemplateV1,
|
@@ -215,12 +242,8 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
|
|
215
242
|
ruaml_instance: yaml.YAML,
|
216
243
|
template_input: TemplateInput,
|
217
244
|
) -> TemplateOutput | None:
|
218
|
-
r =
|
219
|
-
template,
|
220
|
-
TemplateData(
|
221
|
-
variables=variables,
|
222
|
-
),
|
223
|
-
secret_reader=self.secret_reader,
|
245
|
+
r = TemplateRendererIntegration._create_renderer(
|
246
|
+
template, variables, secret_reader=self.secret_reader
|
224
247
|
)
|
225
248
|
target_path = r.render_target_path()
|
226
249
|
|
@@ -12,6 +12,7 @@ from reconcile.gql_definitions.templating.templates import (
|
|
12
12
|
)
|
13
13
|
from reconcile.templating.lib.rendering import Renderer, TemplateData, create_renderer
|
14
14
|
from reconcile.utils import gql
|
15
|
+
from reconcile.utils.jinja2.utils import TemplateRenderOptions
|
15
16
|
from reconcile.utils.ruamel import create_ruamel_instance
|
16
17
|
from reconcile.utils.runtime.integration import (
|
17
18
|
QontractReconcileIntegration,
|
@@ -50,6 +51,17 @@ class TemplateValidatorIntegration(QontractReconcileIntegration):
|
|
50
51
|
current=ruaml_instance.load(template_test.current or ""),
|
51
52
|
),
|
52
53
|
secret_reader=secret_reader,
|
54
|
+
template_render_options=TemplateRenderOptions.create(
|
55
|
+
trim_blocks=template.template_render_options.trim_blocks
|
56
|
+
if template.template_render_options
|
57
|
+
else None,
|
58
|
+
lstrip_blocks=template.template_render_options.lstrip_blocks
|
59
|
+
if template.template_render_options
|
60
|
+
else None,
|
61
|
+
keep_trailing_newline=template.template_render_options.keep_trailing_newline
|
62
|
+
if template.template_render_options
|
63
|
+
else None,
|
64
|
+
),
|
53
65
|
)
|
54
66
|
|
55
67
|
@staticmethod
|
@@ -1085,6 +1085,7 @@ class TestConfigHashPromotionsValidation(TestCase):
|
|
1085
1085
|
"success": True,
|
1086
1086
|
"saas_file": self.saas_file.name,
|
1087
1087
|
"target_config_hash": "ed2af38cf21f268c",
|
1088
|
+
"has_succeeded_once": True,
|
1088
1089
|
}
|
1089
1090
|
self.state_mock.get.return_value = publisher_state
|
1090
1091
|
result = self.saasherder.validate_promotions()
|
@@ -1101,16 +1102,19 @@ class TestConfigHashPromotionsValidation(TestCase):
|
|
1101
1102
|
"success": True,
|
1102
1103
|
"saas_file": self.saas_file.name,
|
1103
1104
|
"target_config_hash": "ed2af38cf21f268c",
|
1105
|
+
"has_succeeded_once": True,
|
1104
1106
|
},
|
1105
1107
|
{
|
1106
1108
|
"success": True,
|
1107
1109
|
"saas_file": self.saas_file.name,
|
1108
1110
|
"target_config_hash": "ed2af38cf21f268c",
|
1111
|
+
"has_succeeded_once": True,
|
1109
1112
|
},
|
1110
1113
|
{
|
1111
1114
|
"success": True,
|
1112
1115
|
"saas_file": self.saas_file.name,
|
1113
1116
|
"target_config_hash": "will_not_match",
|
1117
|
+
"has_succeeded_once": True,
|
1114
1118
|
},
|
1115
1119
|
]
|
1116
1120
|
self.state_mock.get.side_effect = publisher_states
|
@@ -1137,6 +1141,7 @@ class TestConfigHashPromotionsValidation(TestCase):
|
|
1137
1141
|
"success": True,
|
1138
1142
|
"saas_file": self.saas_file.name,
|
1139
1143
|
"target_config_hash": "whatever",
|
1144
|
+
"has_succeeded_once": True,
|
1140
1145
|
}
|
1141
1146
|
|
1142
1147
|
self.assertEqual(len(self.saasherder.promotions), 4)
|
@@ -1148,6 +1153,62 @@ class TestConfigHashPromotionsValidation(TestCase):
|
|
1148
1153
|
result = self.saasherder.validate_promotions()
|
1149
1154
|
self.assertTrue(result)
|
1150
1155
|
|
1156
|
+
def test_promotion_state_re_deployment_failed(self) -> None:
|
1157
|
+
"""A promotion is valid if it has ever succeeded for that ref.
|
1158
|
+
Re-deployment results should be neglected for validation.
|
1159
|
+
"""
|
1160
|
+
publisher_state = {
|
1161
|
+
# Latest state is failed ...
|
1162
|
+
"success": False,
|
1163
|
+
"saas_file": self.saas_file.name,
|
1164
|
+
"target_config_hash": "ed2af38cf21f268c",
|
1165
|
+
# ... however, the deployment succeeded sometime before once.
|
1166
|
+
"has_succeeded_once": True,
|
1167
|
+
}
|
1168
|
+
self.state_mock.get.return_value = publisher_state
|
1169
|
+
result = self.saasherder.validate_promotions()
|
1170
|
+
self.assertTrue(result)
|
1171
|
+
|
1172
|
+
def test_promotion_state_never_successfully_deployed(self) -> None:
|
1173
|
+
"""A promotion is invalid, if it never succeeded before."""
|
1174
|
+
publisher_state = {
|
1175
|
+
# Latest state is failed ...
|
1176
|
+
"success": False,
|
1177
|
+
"saas_file": self.saas_file.name,
|
1178
|
+
"target_config_hash": "ed2af38cf21f268c",
|
1179
|
+
# ... and it never succeeded once before.
|
1180
|
+
"has_succeeded_once": False,
|
1181
|
+
}
|
1182
|
+
self.state_mock.get.return_value = publisher_state
|
1183
|
+
result = self.saasherder.validate_promotions()
|
1184
|
+
self.assertFalse(result)
|
1185
|
+
|
1186
|
+
def test_promotion_state_success_backwards_compatibility_success(self) -> None:
|
1187
|
+
"""Not all states have the has_succeeded_once attribute yet.
|
1188
|
+
If it doesnt exist, we should always fall back to latest success state.
|
1189
|
+
"""
|
1190
|
+
publisher_state = {
|
1191
|
+
"success": True,
|
1192
|
+
"saas_file": self.saas_file.name,
|
1193
|
+
"target_config_hash": "ed2af38cf21f268c",
|
1194
|
+
}
|
1195
|
+
self.state_mock.get.return_value = publisher_state
|
1196
|
+
result = self.saasherder.validate_promotions()
|
1197
|
+
self.assertTrue(result)
|
1198
|
+
|
1199
|
+
def test_promotion_state_success_backwards_compatibility_fail(self) -> None:
|
1200
|
+
"""Not all states have the has_succeeded_once attribute yet.
|
1201
|
+
If it doesnt exist, we should always fall back to latest success state.
|
1202
|
+
"""
|
1203
|
+
publisher_state = {
|
1204
|
+
"success": False,
|
1205
|
+
"saas_file": self.saas_file.name,
|
1206
|
+
"target_config_hash": "ed2af38cf21f268c",
|
1207
|
+
}
|
1208
|
+
self.state_mock.get.return_value = publisher_state
|
1209
|
+
result = self.saasherder.validate_promotions()
|
1210
|
+
self.assertFalse(result)
|
1211
|
+
|
1151
1212
|
|
1152
1213
|
@pytest.mark.usefixtures("inject_gql_class_factory")
|
1153
1214
|
class TestSoakDays(TestCase):
|
reconcile/utils/jinja2/utils.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
from functools import cache
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any, Self
|
3
3
|
|
4
4
|
import jinja2
|
5
5
|
from jinja2.sandbox import SandboxedEnvironment
|
6
|
+
from pydantic import BaseModel
|
6
7
|
from sretoolbox.utils import retry
|
7
8
|
|
8
9
|
from reconcile import queries
|
@@ -31,18 +32,46 @@ class Jinja2TemplateError(Exception):
|
|
31
32
|
super().__init__("error processing jinja2 template: " + str(msg))
|
32
33
|
|
33
34
|
|
35
|
+
class TemplateRenderOptions(BaseModel):
|
36
|
+
trim_blocks: bool
|
37
|
+
lstrip_blocks: bool
|
38
|
+
keep_trailing_newline: bool
|
39
|
+
|
40
|
+
class Config:
|
41
|
+
frozen = True
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def create(
|
45
|
+
cls,
|
46
|
+
trim_blocks: bool | None = None,
|
47
|
+
lstrip_blocks: bool | None = None,
|
48
|
+
keep_trailing_newline: bool | None = None,
|
49
|
+
) -> Self:
|
50
|
+
return cls(
|
51
|
+
trim_blocks=trim_blocks or False,
|
52
|
+
lstrip_blocks=lstrip_blocks or False,
|
53
|
+
keep_trailing_newline=keep_trailing_newline or False,
|
54
|
+
)
|
55
|
+
|
56
|
+
|
34
57
|
@cache
|
35
|
-
def compile_jinja2_template(
|
36
|
-
|
58
|
+
def compile_jinja2_template(
|
59
|
+
body: str,
|
60
|
+
extra_curly: bool = False,
|
61
|
+
template_render_options: TemplateRenderOptions | None = None,
|
62
|
+
) -> Any:
|
63
|
+
if not template_render_options:
|
64
|
+
template_render_options = TemplateRenderOptions.create()
|
65
|
+
env: dict[str, Any] = template_render_options.dict()
|
37
66
|
if extra_curly:
|
38
|
-
env
|
67
|
+
env.update({
|
39
68
|
"block_start_string": "{{%",
|
40
69
|
"block_end_string": "%}}",
|
41
70
|
"variable_start_string": "{{{",
|
42
71
|
"variable_end_string": "}}}",
|
43
72
|
"comment_start_string": "{{#",
|
44
73
|
"comment_end_string": "#}}",
|
45
|
-
}
|
74
|
+
})
|
46
75
|
|
47
76
|
jinja_env = SandboxedEnvironment(
|
48
77
|
extensions=[B64EncodeExtension, RaiseErrorExtension],
|
@@ -154,6 +183,7 @@ def process_jinja2_template(
|
|
154
183
|
extra_curly: bool = False,
|
155
184
|
settings: dict[str, Any] | None = None,
|
156
185
|
secret_reader: SecretReaderBase | None = None,
|
186
|
+
template_render_options: TemplateRenderOptions | None = None,
|
157
187
|
) -> Any:
|
158
188
|
if vars is None:
|
159
189
|
vars = {}
|
@@ -187,7 +217,7 @@ def process_jinja2_template(
|
|
187
217
|
for k, v in vars["_template_mocks"].items():
|
188
218
|
vars[k] = lambda *args, **kwargs: v
|
189
219
|
try:
|
190
|
-
template = compile_jinja2_template(body, extra_curly)
|
220
|
+
template = compile_jinja2_template(body, extra_curly, template_render_options)
|
191
221
|
r = template.render(vars)
|
192
222
|
except Exception as e:
|
193
223
|
raise Jinja2TemplateError(e)
|
@@ -197,9 +227,10 @@ def process_jinja2_template(
|
|
197
227
|
def process_extracurlyjinja2_template(
|
198
228
|
body: str,
|
199
229
|
vars: dict[str, Any] | None = None,
|
200
|
-
extra_curly: bool = True,
|
230
|
+
extra_curly: bool = True, # ignored. Just to be compatible with process_jinja2_template
|
201
231
|
settings: dict[str, Any] | None = None,
|
202
232
|
secret_reader: SecretReaderBase | None = None,
|
233
|
+
template_render_options: TemplateRenderOptions | None = None,
|
203
234
|
) -> Any:
|
204
235
|
if vars is None:
|
205
236
|
vars = {}
|
@@ -209,6 +240,7 @@ def process_extracurlyjinja2_template(
|
|
209
240
|
extra_curly=True,
|
210
241
|
settings=settings,
|
211
242
|
secret_reader=secret_reader,
|
243
|
+
template_render_options=template_render_options,
|
212
244
|
)
|
213
245
|
|
214
246
|
|
@@ -18,10 +18,15 @@ class PromotionData(BaseModel):
|
|
18
18
|
requirements.
|
19
19
|
"""
|
20
20
|
|
21
|
+
# The success is primarily used for SAPM auto-promotions
|
21
22
|
success: bool
|
22
23
|
target_config_hash: str | None
|
23
24
|
saas_file: str | None
|
24
25
|
check_in: str | None
|
26
|
+
# Whether this promotion has ever succeeded
|
27
|
+
# Note, this shouldnt be overridden on subsequent promotions of same ref
|
28
|
+
# This attribute is primarily used by saasherder validations
|
29
|
+
has_succeeded_once: bool | None
|
25
30
|
|
26
31
|
class Config:
|
27
32
|
smart_union = True
|
@@ -1846,7 +1846,9 @@ class SaasHerder: # pylint: disable=too-many-public-methods
|
|
1846
1846
|
target_uid=target_uid,
|
1847
1847
|
pre_check_sha_exists=False,
|
1848
1848
|
)
|
1849
|
-
if not (
|
1849
|
+
if not (
|
1850
|
+
deployment and (deployment.success or deployment.has_succeeded_once)
|
1851
|
+
):
|
1850
1852
|
logging.error(
|
1851
1853
|
f"Commit {promotion.commit_sha} was not "
|
1852
1854
|
+ f"published with success to channel {channel.name}"
|
@@ -1943,6 +1945,17 @@ class SaasHerder: # pylint: disable=too-many-public-methods
|
|
1943
1945
|
all_subscribed_saas_file_paths = set()
|
1944
1946
|
all_subscribed_target_paths = set()
|
1945
1947
|
for channel in promotion.publish:
|
1948
|
+
# make sure we keep some attributes on re-deployments of same ref
|
1949
|
+
has_succeeded_once = success
|
1950
|
+
current_state = self._promotion_state.get_promotion_data(
|
1951
|
+
sha=promotion.commit_sha,
|
1952
|
+
channel=channel,
|
1953
|
+
target_uid=promotion.saas_target_uid,
|
1954
|
+
use_cache=True,
|
1955
|
+
)
|
1956
|
+
if current_state and current_state.has_succeeded_once:
|
1957
|
+
has_succeeded_once = True
|
1958
|
+
|
1946
1959
|
# publish to state to pass promotion gate
|
1947
1960
|
self._promotion_state.publish_promotion_data(
|
1948
1961
|
sha=promotion.commit_sha,
|
@@ -1952,6 +1965,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
|
|
1952
1965
|
saas_file=promotion.saas_file,
|
1953
1966
|
success=success,
|
1954
1967
|
target_config_hash=promotion.target_config_hash,
|
1968
|
+
has_succeeded_once=has_succeeded_once,
|
1955
1969
|
# TODO: do not override - check if timestamp already exists
|
1956
1970
|
check_in=str(now),
|
1957
1971
|
),
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc922.dist-info → qontract_reconcile-0.10.1rc924.dist-info}/top_level.txt
RENAMED
File without changes
|