qontract-reconcile 0.10.1rc923__py3-none-any.whl → 0.10.1rc925__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.1rc923
3
+ Version: 0.10.1rc925
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
@@ -463,11 +463,11 @@ 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=oUUiHHSgD5bcUN3Xf-yoEezAf3LjqN-2GlYhQUTPIZY,13067
466
+ reconcile/templating/renderer.py,sha256=GjjYPDmiP0zvTDxOr_vAniqN9UHcSMiv-biPUvy8NzM,13091
467
467
  reconcile/templating/validator.py,sha256=5f9f35PCHOOdjb7KZquL2YdabyuAUokPDa4xutSEHIQ,5360
468
468
  reconcile/templating/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
469
- reconcile/templating/lib/merge_request_manager.py,sha256=4oe3EwQOP7CZSraocivbRzzOuVb0ooElaUS2_DGsF50,5603
470
- reconcile/templating/lib/model.py,sha256=fb6FYYLQjmoh2DjVKO7TEWCuDPf1Q34xmOx0M9Z07ek,324
469
+ reconcile/templating/lib/merge_request_manager.py,sha256=P4IbblzXbubjlaxAYoahHFADUNIXLepfqwzdnHPs8OY,5608
470
+ reconcile/templating/lib/model.py,sha256=Y1PeEP-koHKiH46JSwv4-PGRGQtcxZa9VmjTiuI6Lxs,889
471
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
@@ -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=f_8W8RC5di6IgiqtFaeRi6E9RJ43MGU2zFsLR_p30FQ,54524
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=Sd3otIb54vgpEe0JhFG1tkXhPS51LItmkI19pZJhDbE,3607
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
@@ -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=H3pLQAD6EkjwrFpfQN73LKPg8mYRilssIcZ9M3duizU,84555
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.1rc923.dist-info/METADATA,sha256=OjR8Y-8f7JvaPxAt_N-8baMq5aI7LwcbOL2Cirs809I,2273
848
- qontract_reconcile-0.10.1rc923.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
849
- qontract_reconcile-0.10.1rc923.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
850
- qontract_reconcile-0.10.1rc923.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
851
- qontract_reconcile-0.10.1rc923.dist-info/RECORD,,
847
+ qontract_reconcile-0.10.1rc925.dist-info/METADATA,sha256=JJstPmbQ_muKOcYIt2w0XSiI3q4MlNNDRsJ0enydTfk,2273
848
+ qontract_reconcile-0.10.1rc925.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
849
+ qontract_reconcile-0.10.1rc925.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
850
+ qontract_reconcile-0.10.1rc925.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
851
+ qontract_reconcile-0.10.1rc925.dist-info/RECORD,,
@@ -136,7 +136,7 @@ class MergeRequestManager(MergeRequestManagerBase[TemplateInfo]):
136
136
 
137
137
  output = data.data
138
138
  collections = {o.input.collection for o in output}
139
- collection_hashes = {o.input.collection_hash for o in output}
139
+ collection_hashes = {o.input.calc_template_hash() for o in output}
140
140
  additional_labels = {label for o in output for label in o.input.labels}
141
141
  # From the way the code is written, we can assert that there is only one collection and one template hash
142
142
  assert len(collections) == 1
@@ -1,12 +1,30 @@
1
+ from typing import Any
2
+
3
+ from deepdiff import DeepHash
1
4
  from pydantic import BaseModel
2
5
 
6
+ from reconcile.gql_definitions.templating.template_collection import (
7
+ TemplateV1,
8
+ )
9
+
3
10
 
4
11
  class TemplateInput(BaseModel):
5
12
  collection: str
6
- collection_hash: str
13
+ templates: list[TemplateV1] = []
14
+ variables: list[dict[str, Any]] = []
15
+ collection_hash: str = ""
7
16
  enable_auto_approval: bool = False
8
17
  labels: list[str] = []
9
18
 
19
+ def calc_template_hash(self) -> str:
20
+ if not self.collection_hash:
21
+ hashable = {
22
+ "templates": sorted(self.templates, key=lambda x: x.name),
23
+ "variables": self.variables,
24
+ }
25
+ self.collection_hash = DeepHash(hashable)[hashable]
26
+ return self.collection_hash
27
+
10
28
 
11
29
  class TemplateOutput(BaseModel):
12
30
  input: TemplateInput
@@ -7,7 +7,6 @@ from collections.abc import Callable
7
7
  from pathlib import Path
8
8
  from typing import Any, Self
9
9
 
10
- from deepdiff import DeepHash
11
10
  from ruamel import yaml
12
11
 
13
12
  from reconcile.gql_definitions.templating.template_collection import (
@@ -187,14 +186,6 @@ def unpack_dynamic_variables(
187
186
  return dynamic
188
187
 
189
188
 
190
- def calc_template_hash(c: TemplateCollectionV1, variables: dict[str, Any]) -> str:
191
- hashable = {
192
- "templates": sorted(c.templates, key=lambda x: x.name),
193
- "variables": variables,
194
- }
195
- return DeepHash(hashable)[hashable]
196
-
197
-
198
189
  class TemplateRendererIntegrationParams(PydanticRunParams):
199
190
  clone_repo: bool = False
200
191
  app_interface_data_path: str | None
@@ -276,11 +267,11 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
276
267
  self,
277
268
  collection: TemplateCollectionV1,
278
269
  gql_api: GqlApi,
279
- dry_run: bool,
280
270
  persistence: FilePersistence,
281
271
  ruamel_instance: yaml.YAML,
272
+ input: TemplateInput,
282
273
  each: dict[str, Any],
283
- ) -> None:
274
+ ) -> list[TemplateOutput]:
284
275
  variables = {}
285
276
  if collection.variables:
286
277
  variables = {
@@ -289,20 +280,17 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
289
280
  ),
290
281
  "static": unpack_static_variables(collection.variables, each),
291
282
  }
283
+ input.variables.append(variables)
292
284
 
293
- with PersistenceTransaction(persistence, dry_run) as p:
294
- input = TemplateInput(
295
- collection=collection.name,
296
- collection_hash=calc_template_hash(collection, variables),
297
- enable_auto_approval=collection.enable_auto_approval or False,
298
- labels=collection.additional_mr_labels or [],
285
+ outputs: list[TemplateOutput] = []
286
+ for template in collection.templates:
287
+ output = self.process_template(
288
+ template, variables, persistence, ruamel_instance, input
299
289
  )
300
- for template in collection.templates:
301
- output = self.process_template(
302
- template, variables, p, ruamel_instance, input
303
- )
304
- if not dry_run and output:
305
- p.write([output])
290
+ if output:
291
+ outputs.append(output)
292
+
293
+ return outputs
306
294
 
307
295
  def reconcile(
308
296
  self,
@@ -317,15 +305,27 @@ class TemplateRendererIntegration(QontractReconcileIntegration):
317
305
  for_each_items: list[dict[str, Any]] = [{}]
318
306
  if c.for_each and c.for_each.items:
319
307
  for_each_items = c.for_each.items
320
- for item in for_each_items:
321
- self.reconcile_template_collection(
322
- c,
323
- gql_no_validation,
324
- dry_run,
325
- persistence,
326
- ruamel_instance,
327
- item,
328
- )
308
+ input = TemplateInput(
309
+ collection=c.name,
310
+ templates=c.templates,
311
+ enable_auto_approval=c.enable_auto_approval or False,
312
+ labels=c.additional_mr_labels or [],
313
+ )
314
+ with PersistenceTransaction(persistence, dry_run) as p:
315
+ outputs: list[TemplateOutput] = []
316
+ for item in for_each_items:
317
+ outputs.extend(
318
+ self.reconcile_template_collection(
319
+ c,
320
+ gql_no_validation,
321
+ p,
322
+ ruamel_instance,
323
+ input,
324
+ item,
325
+ )
326
+ )
327
+ if not dry_run and outputs:
328
+ p.write(outputs)
329
329
 
330
330
  @property
331
331
  def name(self) -> str:
@@ -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):
@@ -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 (deployment and deployment.success):
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
  ),