qontract-reconcile 0.10.1rc850__py3-none-any.whl → 0.10.1rc852__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.1rc850
3
+ Version: 0.10.1rc852
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
@@ -426,7 +426,7 @@ reconcile/saas_auto_promotions_manager/integration.py,sha256=8IXLEvpblgZRr2UPfTs
426
426
  reconcile/saas_auto_promotions_manager/meta.py,sha256=76Jp50r6Y_KyJoXFfSjrt5YrCtXyg_A4FXXxHYiS3TE,161
427
427
  reconcile/saas_auto_promotions_manager/publisher.py,sha256=am8b0uLZIhWjSAgUHPtSOu2fRpa5wXaMl_WnVe4gv2k,3009
428
428
  reconcile/saas_auto_promotions_manager/s3_exporter.py,sha256=IKlVWZmiPnvl7sKeF6JgAlhXZe5CovKTxQc0SNkNSx4,2583
429
- reconcile/saas_auto_promotions_manager/subscriber.py,sha256=gz6l6xSeyYrs2KyEcXJoYbiH5JGSWE_T3VIqWl03A7k,10501
429
+ reconcile/saas_auto_promotions_manager/subscriber.py,sha256=zAqifs7LxssQurO9UwWNrUnOqomRLuD_Z7nTXhU490o,8519
430
430
  reconcile/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
431
431
  reconcile/saas_auto_promotions_manager/merge_request_manager/batcher.py,sha256=lHIULPE8nmPN9pJVfZFdD0l48EB20o4cAy4owahJenw,7848
432
432
  reconcile/saas_auto_promotions_manager/merge_request_manager/desired_state.py,sha256=isY8frVsL3PlcdZmdZ4O0qyp76oczl4DUMX9uMArs5Y,1222
@@ -532,7 +532,7 @@ reconcile/test/test_quay_repos.py,sha256=TdkcRF_a8PLp01Kti9eZZN-vGup2yPBT4Iba3k0
532
532
  reconcile/test/test_queries.py,sha256=SpH3RmNpBjEr_ne3VjAMCgKK8RE1z1zo7bypkT5uoO4,1946
533
533
  reconcile/test/test_repo_owners.py,sha256=uRYMLbMmh-9usF0TerabZTZV-Z1CS4I6ybT-LQqCLe8,1423
534
534
  reconcile/test/test_requests_sender.py,sha256=7fd9C2kEFS0-CYtlsif66N1kO9c44pzuBPAJKR9igqU,5385
535
- reconcile/test/test_saasherder.py,sha256=hSZk34aZFq-wspT-kJmFuHjh8ztSr7IzGc5QdRVrsJ8,47164
535
+ reconcile/test/test_saasherder.py,sha256=QFX6JrPCpB9jS-K_VleIjTf6NaF8NDa4UwddXXo1yPk,52127
536
536
  reconcile/test/test_saasherder_allowed_secret_paths.py,sha256=5NHQwNJO66at6HiyMZ5sVRTQDwxdvlOQo0KmkBWCw5Q,4853
537
537
  reconcile/test/test_secret_reader.py,sha256=kz7nzcPjvA08cytnvcA_PMA98AEyqJWsESkYeRn5xCk,4994
538
538
  reconcile/test/test_slack_base.py,sha256=gpbWOLNxMMX6fyAbs1JakhLTnwfedb3f7WpUae4tQZE,5060
@@ -784,9 +784,9 @@ reconcile/utils/runtime/meta.py,sha256=X44HzyXIBprf3zcsGr2XLCgoeFkz6r3U2nlFXM1H7
784
784
  reconcile/utils/runtime/runner.py,sha256=72cc-I6yXyPov8UCLHpyERRy1eiMLpGite2roO0yUlo,7979
785
785
  reconcile/utils/runtime/sharding.py,sha256=roCdbnBklhTK_g34zbgQYqzpKPaNQ8J6Xd9XLO9-t6Q,16258
786
786
  reconcile/utils/saasherder/__init__.py,sha256=J3MBZBFa5YmhqYm08QsjBXz8mFcVOCiOCkyIcw41t7E,343
787
- reconcile/utils/saasherder/interfaces.py,sha256=XXY35h8VWQ66z3LBPxaoUAMkIW50264DQiecrzyV6oA,9076
788
- reconcile/utils/saasherder/models.py,sha256=1DKXUmiTS_MejUfSpFCeuBLMTgR4ldv2N1tAz8qHAwc,5547
789
- reconcile/utils/saasherder/saasherder.py,sha256=5rKp1ONBgRgfRO9tMHheEHXjQbipqWB8kUALSCKMGDc,86545
787
+ reconcile/utils/saasherder/interfaces.py,sha256=Tte-BAJ71FZF1J_ADay1UVIxLCJZcbefq4SRua4mn5w,9141
788
+ reconcile/utils/saasherder/models.py,sha256=XiAb9pSmTxaNFa3XqNNfe1JxlGgTqsmd1nLi17iIV_g,5566
789
+ reconcile/utils/saasherder/saasherder.py,sha256=4S3Q4tb11hmBI3rux5ajk-BUs-6Lfpg47uHZJj2uNKk,86787
790
790
  reconcile/utils/terraform/__init__.py,sha256=zNbiyTWo35AT1sFTElL2j_AA0jJ_yWE_bfFn-nD2xik,250
791
791
  reconcile/utils/terraform/config.py,sha256=5UVrd563TMcvi4ooa5JvWVDW1I3bIWg484u79evfV_8,164
792
792
  reconcile/utils/terraform/config_client.py,sha256=py-Ree-QUYD6Hvng6bM40VgSuttteehIKNgwOSoJO1o,4706
@@ -834,8 +834,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
834
834
  tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
835
835
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
836
836
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
837
- qontract_reconcile-0.10.1rc850.dist-info/METADATA,sha256=2AQhJlZY2SAm-W89c68dz2HN_BAkeautQ3Zafjb4y9o,2273
838
- qontract_reconcile-0.10.1rc850.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
839
- qontract_reconcile-0.10.1rc850.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
840
- qontract_reconcile-0.10.1rc850.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
841
- qontract_reconcile-0.10.1rc850.dist-info/RECORD,,
837
+ qontract_reconcile-0.10.1rc852.dist-info/METADATA,sha256=iKUIKevX3oHc70SMvq6rkwA29a-RH-fO9SncYOwTdQ4,2273
838
+ qontract_reconcile-0.10.1rc852.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
839
+ qontract_reconcile-0.10.1rc852.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
840
+ qontract_reconcile-0.10.1rc852.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
841
+ qontract_reconcile-0.10.1rc852.dist-info/RECORD,,
@@ -1,9 +1,9 @@
1
1
  import hashlib
2
2
  import logging
3
- from collections.abc import Iterable, Mapping
3
+ from collections.abc import Iterable
4
4
  from dataclasses import dataclass
5
5
  from datetime import datetime, timedelta, timezone
6
- from typing import Any, Optional
6
+ from typing import Optional
7
7
 
8
8
  from reconcile.gql_definitions.fragments.saas_target_namespace import (
9
9
  SaasTargetNamespace,
@@ -78,56 +78,6 @@ class Subscriber:
78
78
  self._compute_desired_ref()
79
79
  self._compute_desired_config_hashes()
80
80
 
81
- @staticmethod
82
- def from_exported_dict(data: Mapping[str, Any]) -> "Subscriber":
83
- subscriber = Subscriber(
84
- saas_name=data["1"],
85
- template_name=data["2"],
86
- ref=data["3"],
87
- target_file_path=data["4"],
88
- use_target_config_hash=data["5"],
89
- target_namespace=SaasTargetNamespace(**data["6"]),
90
- uid=data["7"],
91
- soak_days=data["8"],
92
- )
93
- subscriber.desired_hashes = data["9"]
94
- subscriber.desired_ref = data["10"]
95
- return subscriber
96
-
97
- def to_exportable_dict(self) -> dict[str, Any]:
98
- """
99
- We will later persist subscriber data as json in MRs. We keep key size small to use less space.
100
- Note, the data will be encoded and encrypted in another component.
101
- """
102
- data: dict[str, Any] = {}
103
- data["1"] = self.saas_name
104
- data["2"] = self.template_name
105
- data["3"] = self.ref
106
- data["4"] = self.target_file_path
107
- data["5"] = self._use_target_config_hash
108
- data["6"] = self.target_namespace.dict(by_alias=True)
109
- data["7"] = self.uid
110
- data["8"] = self.soak_days
111
- data["9"] = self.desired_hashes
112
- data["10"] = self.desired_ref
113
- return data
114
-
115
- def __eq__(self, other: object) -> bool:
116
- if not isinstance(other, Subscriber):
117
- # don't attempt to compare against unrelated types
118
- return False
119
- return (
120
- self.saas_name == other.saas_name
121
- and self.template_name == other.template_name
122
- and self.ref == other.ref
123
- and self.target_file_path == other.target_file_path
124
- and self._use_target_config_hash == other._use_target_config_hash
125
- and self.desired_ref == other.desired_ref
126
- and self.desired_hashes == other.desired_hashes
127
- and self.target_namespace == other.target_namespace
128
- and self.uid == other.uid
129
- )
130
-
131
81
  def _validate_deployment(
132
82
  self, publisher: Publisher, channel: Channel
133
83
  ) -> Optional[DeploymentInfo]:
@@ -3,6 +3,7 @@ from collections.abc import (
3
3
  Iterable,
4
4
  MutableMapping,
5
5
  )
6
+ from datetime import datetime, timedelta, timezone
6
7
  from typing import (
7
8
  Any,
8
9
  Optional,
@@ -1026,7 +1027,7 @@ class TestConfigHashPromotionsValidation(TestCase):
1026
1027
 
1027
1028
  def setUp(self) -> None:
1028
1029
  self.saas_file = self.gql_class_factory( # type: ignore[attr-defined] # it's set in the fixture
1029
- SaasFile, Fixtures("saasherder").get_anymarkup("saas.gql.yml")
1030
+ SaasFile, Fixtures("saasherder").get_anymarkup("saas-multi-channel.gql.yml")
1030
1031
  )
1031
1032
  self.state_patcher = patch("reconcile.utils.state.State", autospec=True)
1032
1033
  self.state_mock = self.state_patcher.start().return_value
@@ -1095,12 +1096,24 @@ class TestConfigHashPromotionsValidation(TestCase):
1095
1096
  promotion_data. This could happen if the parent target has run again
1096
1097
  with the same ref before the subscriber target promotion MR is merged.
1097
1098
  """
1098
- publisher_state = {
1099
- "success": True,
1100
- "saas_file": self.saas_file.name,
1101
- "target_config_hash": "will_not_match",
1102
- }
1103
- self.state_mock.get.return_value = publisher_state
1099
+ publisher_states = [
1100
+ {
1101
+ "success": True,
1102
+ "saas_file": self.saas_file.name,
1103
+ "target_config_hash": "ed2af38cf21f268c",
1104
+ },
1105
+ {
1106
+ "success": True,
1107
+ "saas_file": self.saas_file.name,
1108
+ "target_config_hash": "ed2af38cf21f268c",
1109
+ },
1110
+ {
1111
+ "success": True,
1112
+ "saas_file": self.saas_file.name,
1113
+ "target_config_hash": "will_not_match",
1114
+ },
1115
+ ]
1116
+ self.state_mock.get.side_effect = publisher_states
1104
1117
  result = self.saasherder.validate_promotions()
1105
1118
  self.assertFalse(result)
1106
1119
 
@@ -1126,16 +1139,133 @@ class TestConfigHashPromotionsValidation(TestCase):
1126
1139
  "target_config_hash": "whatever",
1127
1140
  }
1128
1141
 
1129
- self.assertEqual(len(self.saasherder.promotions), 2)
1130
- self.assertIsNotNone(self.saasherder.promotions[1])
1142
+ self.assertEqual(len(self.saasherder.promotions), 4)
1143
+ self.assertIsNotNone(self.saasherder.promotions[3])
1131
1144
  # Remove promotion_data on the promoted target
1132
- self.saasherder.promotions[1].promotion_data = None # type: ignore
1145
+ self.saasherder.promotions[3].promotion_data = None # type: ignore
1133
1146
 
1134
1147
  self.state_mock.get.return_value = publisher_state
1135
1148
  result = self.saasherder.validate_promotions()
1136
1149
  self.assertTrue(result)
1137
1150
 
1138
1151
 
1152
+ @pytest.mark.usefixtures("inject_gql_class_factory")
1153
+ class TestSoakDays(TestCase):
1154
+ """TestCase to test SaasHerder soakDays gate. SaasHerder is
1155
+ initialized with ResourceInventory population. Like is done in
1156
+ openshift-saas-deploy"""
1157
+
1158
+ cluster: str
1159
+ namespace: str
1160
+ fxt: Any
1161
+ template: Any
1162
+
1163
+ @classmethod
1164
+ def setUpClass(cls) -> None:
1165
+ cls.fxt = Fixtures("saasherder")
1166
+ cls.cluster = "test-cluster"
1167
+ cls.template = cls.fxt.get_anymarkup("template_1.yml")
1168
+
1169
+ def setUp(self) -> None:
1170
+ self.saas_file = self.gql_class_factory( # type: ignore[attr-defined] # it's set in the fixture
1171
+ SaasFile, Fixtures("saasherder").get_anymarkup("saas-soak-days.gql.yml")
1172
+ )
1173
+ self.state_patcher = patch("reconcile.utils.state.State", autospec=True)
1174
+ self.state_mock = self.state_patcher.start().return_value
1175
+
1176
+ self.ig_patcher = patch.object(SaasHerder, "_initiate_github", autospec=True)
1177
+ self.ig_patcher.start()
1178
+
1179
+ self.image_auth_patcher = patch.object(SaasHerder, "_initiate_image_auth")
1180
+ self.image_auth_patcher.start()
1181
+
1182
+ self.gfc_patcher = patch.object(SaasHerder, "_get_file_contents", autospec=True)
1183
+ gfc_mock = self.gfc_patcher.start()
1184
+ gfc_mock.return_value = (self.template, "url", "ahash")
1185
+
1186
+ self.deploy_current_state_fxt = self.fxt.get_anymarkup("saas_deploy.state.json")
1187
+
1188
+ self.post_deploy_current_state_fxt = self.fxt.get_anymarkup(
1189
+ "saas_post_deploy.state.json"
1190
+ )
1191
+
1192
+ self.saasherder = SaasHerder(
1193
+ [self.saas_file],
1194
+ secret_reader=MockSecretReader(),
1195
+ thread_pool_size=1,
1196
+ state=self.state_mock,
1197
+ integration="",
1198
+ integration_version="",
1199
+ hash_length=24,
1200
+ repo_url="https://repo-url.com",
1201
+ all_saas_files=[self.saas_file],
1202
+ )
1203
+
1204
+ # IMPORTANT: Populating desired state modify self.saas_files within
1205
+ # saasherder object.
1206
+ self.ri = ResourceInventory()
1207
+ for ns in ["test-ns-publisher", "test-ns-subscriber"]:
1208
+ for kind in ["Service", "Deployment"]:
1209
+ self.ri.initialize_resource_type(self.cluster, ns, kind)
1210
+
1211
+ self.saasherder.populate_desired_state(self.ri)
1212
+ if self.ri.has_error_registered():
1213
+ raise Exception("Errors registered in Resourceinventory")
1214
+
1215
+ def tearDown(self) -> None:
1216
+ self.state_patcher.stop()
1217
+ self.ig_patcher.stop()
1218
+ self.gfc_patcher.stop()
1219
+
1220
+ def test_soak_days_passed(self) -> None:
1221
+ """A promotion is valid if the parent targets accumulated soak_days
1222
+ passed. We have a soakDays setting of 2 days.
1223
+ """
1224
+ publisher_states = [
1225
+ {
1226
+ "success": True,
1227
+ "saas_file": self.saas_file.name,
1228
+ "target_config_hash": "ed2af38cf21f268c",
1229
+ # the deployment happened 1 hour ago
1230
+ "check_in": str(datetime.now(timezone.utc) - timedelta(hours=1)),
1231
+ },
1232
+ {
1233
+ "success": True,
1234
+ "saas_file": self.saas_file.name,
1235
+ "target_config_hash": "ed2af38cf21f268c",
1236
+ # the deployment happened 47 hours ago
1237
+ "check_in": str(datetime.now(timezone.utc) - timedelta(hours=47)),
1238
+ },
1239
+ ]
1240
+ self.state_mock.get.side_effect = publisher_states
1241
+ result = self.saasherder.validate_promotions()
1242
+ self.assertTrue(result)
1243
+
1244
+ def test_soak_days_not_passed(self) -> None:
1245
+ """A promotion is valid if the parent target accumulated soak_days
1246
+ passed. We have a soakDays setting of 2 days.
1247
+ """
1248
+ publisher_states = [
1249
+ {
1250
+ "success": True,
1251
+ "saas_file": self.saas_file.name,
1252
+ "target_config_hash": "ed2af38cf21f268c",
1253
+ # the deployment happened 12 hours ago
1254
+ "check_in": str(datetime.now(timezone.utc) - timedelta(hours=12)),
1255
+ },
1256
+ {
1257
+ "success": True,
1258
+ "saas_file": self.saas_file.name,
1259
+ "target_config_hash": "ed2af38cf21f268c",
1260
+ # the deployment happened 1 hour ago
1261
+ "check_in": str(datetime.now(timezone.utc) - timedelta(hours=1)),
1262
+ },
1263
+ ]
1264
+ self.state_mock.get.side_effect = publisher_states
1265
+ result = self.saasherder.validate_promotions()
1266
+ self.assertFalse(result)
1267
+
1268
+
1139
1269
  @pytest.mark.usefixtures("inject_gql_class_factory")
1140
1270
  class TestConfigHashTrigger(TestCase):
1141
1271
  """TestCase to test Openshift SAAS deploy configs trigger. SaasHerder is
@@ -242,6 +242,7 @@ class SaasResourceTemplateTargetPromotion(Protocol):
242
242
  auto: Optional[bool]
243
243
  publish: Optional[list[str]]
244
244
  subscribe: Optional[list[str]]
245
+ soak_days: Optional[int]
245
246
 
246
247
  @property
247
248
  def promotion_data(self) -> Optional[Sequence[SaasPromotionData]]: ...
@@ -261,6 +262,7 @@ class SaasPromotion(Protocol):
261
262
  publish: Optional[list[str]] = None
262
263
  saas_file_paths: Optional[list[str]] = None
263
264
  target_paths: Optional[list[str]] = None
265
+ soak_days: Optional[int] = None
264
266
 
265
267
  @property
266
268
  def promotion_data(self) -> Optional[Sequence[SaasPromotionData]]: ...
@@ -179,6 +179,7 @@ class Promotion(BaseModel):
179
179
  saas_file: str
180
180
  target_config_hash: str
181
181
  saas_target_uid: str
182
+ soak_days: int
182
183
  auto: Optional[bool] = None
183
184
  publish: Optional[list[str]] = None
184
185
  subscribe: Optional[list[Channel]] = None
@@ -16,7 +16,7 @@ from collections.abc import (
16
16
  Sequence,
17
17
  )
18
18
  from contextlib import suppress
19
- from datetime import datetime, timezone
19
+ from datetime import datetime, timedelta, timezone
20
20
  from types import TracebackType
21
21
  from typing import (
22
22
  Any,
@@ -1068,6 +1068,7 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1068
1068
  parent_resource_template_name=resource_template_name,
1069
1069
  parent_saas_file_name=saas_file_name,
1070
1070
  ),
1071
+ soak_days=target.promotion.soak_days or 0,
1071
1072
  )
1072
1073
  return resources, html_url, target_promotion
1073
1074
 
@@ -1880,82 +1881,98 @@ class SaasHerder: # pylint: disable=too-many-public-methods
1880
1881
  # TODO: add environment.parameters to the include list!?!?
1881
1882
  )
1882
1883
 
1883
- def validate_promotions(self) -> bool:
1884
- """
1885
- If there were promotion sections in the participating saas files
1886
- validate that the conditions are met."""
1884
+ def _validate_promotion(self, promotion: Promotion) -> bool:
1885
+ # Placing this check here to make mypy happy
1887
1886
  if not (self.state and self._promotion_state):
1888
1887
  raise Exception("state is not initialized")
1889
1888
 
1890
- for promotion in self.promotions:
1891
- if promotion is None:
1892
- continue
1893
- # validate that the commit sha being promoted
1894
- # was successfully published to the subscribed channel(s)
1895
- if promotion.subscribe:
1896
- for channel in promotion.subscribe:
1897
- config_hashes: set[str] = set()
1898
- for target_uid in channel.publisher_uids:
1899
- deployment = self._promotion_state.get_promotion_data(
1900
- sha=promotion.commit_sha,
1901
- channel=channel.name,
1902
- target_uid=target_uid,
1903
- local_lookup=False,
1904
- )
1905
- if not (deployment and deployment.success):
1906
- logging.error(
1907
- f"Commit {promotion.commit_sha} was not "
1908
- + f"published with success to channel {channel.name}"
1909
- )
1910
- return False
1911
- if deployment.target_config_hash:
1912
- config_hashes.add(deployment.target_config_hash)
1913
-
1914
- # This code supports current saas targets that does
1915
- # not have promotion_data yet
1916
- if not config_hashes or not promotion.promotion_data:
1917
- logging.info(
1918
- "Promotion data is missing; rely on the success "
1919
- "state only"
1920
- )
1921
- return True
1922
-
1923
- # Validate the promotion_data section.
1924
- # Just validate parent_saas_config hash
1925
- # promotion_data type by now.
1926
- parent_saas_config = None
1927
- for pd in promotion.promotion_data:
1928
- if pd.channel == channel.name:
1929
- for data in pd.data or []:
1930
- if isinstance(data, SaasParentSaasPromotion):
1931
- parent_saas_config = data
1932
-
1933
- # This section might not exist due to a manual MR.
1934
- # Promotion shall continue if this data is missing.
1935
- # The parent at the same ref has succeed if this code
1936
- # is reached though.
1937
- if not parent_saas_config:
1938
- logging.info(
1939
- "Parent Saas config missing on target "
1940
- "rely on the success state only"
1941
- )
1942
- return True
1943
-
1944
- # Validate that the state config_hash set by the parent
1945
- # matches with the hash set in promotion_data
1946
- if parent_saas_config.target_config_hash in config_hashes:
1947
- return True
1889
+ if not promotion.subscribe:
1890
+ return True
1948
1891
 
1892
+ now = datetime.now(timezone.utc)
1893
+ passed_soak_days = timedelta(days=0)
1894
+
1895
+ for channel in promotion.subscribe:
1896
+ config_hashes: set[str] = set()
1897
+ for target_uid in channel.publisher_uids:
1898
+ deployment = self._promotion_state.get_promotion_data(
1899
+ sha=promotion.commit_sha,
1900
+ channel=channel.name,
1901
+ target_uid=target_uid,
1902
+ local_lookup=False,
1903
+ )
1904
+ if not (deployment and deployment.success):
1949
1905
  logging.error(
1950
- "Parent saas target has run with a newer "
1951
- "configuration and the same commit (ref). "
1952
- "Check if other MR exists for this target, "
1953
- f"or update {parent_saas_config.target_config_hash} "
1954
- f"to any in {config_hashes} for channel {channel.name}"
1906
+ f"Commit {promotion.commit_sha} was not "
1907
+ + f"published with success to channel {channel.name}"
1955
1908
  )
1956
1909
  return False
1910
+ if check_in := deployment.check_in:
1911
+ passed_soak_days += now - datetime.fromisoformat(check_in)
1912
+ if deployment.target_config_hash:
1913
+ config_hashes.add(deployment.target_config_hash)
1914
+
1915
+ # This code supports current saas targets that does
1916
+ # not have promotion_data yet
1917
+ if not config_hashes or not promotion.promotion_data:
1918
+ logging.info(
1919
+ "Promotion data is missing; rely on the success " "state only"
1920
+ )
1921
+ continue
1922
+
1923
+ # Validate the promotion_data section.
1924
+ # Just validate parent_saas_config hash
1925
+ # promotion_data type by now.
1926
+ parent_saas_config = None
1927
+ for pd in promotion.promotion_data:
1928
+ if pd.channel == channel.name:
1929
+ for data in pd.data or []:
1930
+ if isinstance(data, SaasParentSaasPromotion):
1931
+ parent_saas_config = data
1932
+
1933
+ # This section might not exist due to a manual MR.
1934
+ # Promotion shall continue if this data is missing.
1935
+ # The parent at the same ref has succeed if this code
1936
+ # is reached though.
1937
+ if not parent_saas_config:
1938
+ logging.info(
1939
+ "Parent Saas config missing on target "
1940
+ "rely on the success state only"
1941
+ )
1942
+ continue
1943
+
1944
+ # Validate that the state config_hash set by the parent
1945
+ # matches with the hash set in promotion_data
1946
+ if parent_saas_config.target_config_hash in config_hashes:
1947
+ continue
1948
+
1949
+ logging.error(
1950
+ "Parent saas target has run with a newer "
1951
+ "configuration and the same commit (ref). "
1952
+ "Check if other MR exists for this target, "
1953
+ f"or update {parent_saas_config.target_config_hash} "
1954
+ f"to any in {config_hashes} for channel {channel.name}"
1955
+ )
1956
+ return False
1957
+
1958
+ if passed_soak_days < timedelta(days=promotion.soak_days):
1959
+ logging.error(
1960
+ f"SoakDays in publishers did not pass. So far accumulated soakDays is {passed_soak_days},"
1961
+ f"but we have a soakDays setting of {promotion.soak_days}. We cannot proceed with this promotion."
1962
+ )
1963
+ return False
1957
1964
  return True
1958
1965
 
1966
+ def validate_promotions(self) -> bool:
1967
+ """
1968
+ If there were promotion sections in the participating saas files
1969
+ validate that the conditions are met."""
1970
+ return all(
1971
+ self._validate_promotion(promotion)
1972
+ for promotion in self.promotions
1973
+ if promotion is not None
1974
+ )
1975
+
1959
1976
  def publish_promotions(
1960
1977
  self,
1961
1978
  success: bool,