qontract-reconcile 0.10.1rc495__py3-none-any.whl → 0.10.1rc497__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.1rc495
3
+ Version: 0.10.1rc497
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
@@ -340,13 +340,13 @@ reconcile/rhidp/sso_client/base.py,sha256=EfQ2ewcOKh5idg46UKAkY6z0m_nGQfvnQKffa2
340
340
  reconcile/rhidp/sso_client/integration.py,sha256=kA8g7c38ZBSdrRtyfEqy_WgSreD1PbwY7ZIN-3tZRPc,2221
341
341
  reconcile/rhidp/sso_client/metrics.py,sha256=Tq7tSOsqL3XdcPUdozxqzSPIodUeOV87UCTqpuuqqhw,1013
342
342
  reconcile/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
343
- reconcile/saas_auto_promotions_manager/integration.py,sha256=1_e9LX-9oMHiuWPlQZ34WbsrxfW0IFANZtV_jXKW_PQ,5580
343
+ reconcile/saas_auto_promotions_manager/integration.py,sha256=lRDMLekNMhPaMJFKS8e2ueK3EpYTC3T199uMoJck_zA,5503
344
344
  reconcile/saas_auto_promotions_manager/publisher.py,sha256=4_M9Oykhj-kEZPUn05E2DY5gD6-x32Dgf7K3NPOkGEg,2029
345
345
  reconcile/saas_auto_promotions_manager/subscriber.py,sha256=cLhPlkT71J2LIice3SLmH1WpsqzV46gd0peMxrnqyRw,7452
346
346
  reconcile/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
347
347
  reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request.py,sha256=PNu7sE-tDUY61E03z5w0b93fdowZ8auCl0S4_vhYOKQ,1333
348
- reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py,sha256=rNyItd0fMxruxjlaP5yUmvv7di5ZGHVnqMjkvxSiI3c,10970
349
- reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py,sha256=bJfmrBaHkY5o5m56d-L8HNnVrOkBJqJ7yr14Do8yCuI,7088
348
+ reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py,sha256=9boB6yp5Fin6XUCtNNsFVIOExi2o7IfbRbG9o1ZAe5Q,12797
349
+ reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py,sha256=Sj3mwmBcml6t-49bPnXdq7snT9N0WKrPk2chU_0ylyE,7188
350
350
  reconcile/saas_auto_promotions_manager/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
351
351
  reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py,sha256=mihuWynroB1Cea1Lsvf6V8Nb8PGiBdcLC0uhCX_52Y0,6966
352
352
  reconcile/skupper_network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -446,10 +446,10 @@ reconcile/test/saas_auto_promotions_manager/conftest.py,sha256=tZNs35EuWulP53Cqt
446
446
  reconcile/test/saas_auto_promotions_manager/test_integration_test.py,sha256=qip-D7a97VnX_-vvDdqJDXeyGDBSG2YvIjI3j7MY1hQ,3901
447
447
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
448
448
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
449
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py,sha256=oWDqepAOJ80VSlk_H0sMBUm0OLqJ3l0wEmg1jzVBS5U,3480
450
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py,sha256=CTQc6Gesh0_8OGPip0SGamrrvFVa4Qo4pJzP-aNh4fE,496
451
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_housekeeping.py,sha256=Yvt8G-j3Ual8RmlaE0M2LXHH44Av7QKIBg0ZJkv24eE,6114
452
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py,sha256=7jeN9WJADulxlwFS0G8-_Avzfw3NwyHMVIdEuGoVfOg,6279
449
+ reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py,sha256=9N5M_uZn6GGbUDfqLkj9RarrKo_qUZVJDp26bH6m2Gw,3597
450
+ reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py,sha256=vkBpEBkx9mcYN4VkzijTJobfSUbgg5TxHOFpWbs0ztQ,535
451
+ reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py,sha256=16QPUcg0TR1gXS_ELVcJ3frpYb9PBW55c6FRbS5i0y8,6078
452
+ reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_mr_parsing.py,sha256=HC-uyM3VVgA02Fkc0T0XE-eg47noyA9MB_gbKMeufh4,8829
453
453
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
454
454
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py,sha256=2rCSstewp4LPoEJHm5N7dGJexEtY8ndLHvoGZYjmpsc,1678
455
455
  reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py,sha256=beHYQ9kgDLeBZgC2FvxQA3tHx1PO-RAMN8_kVcSdikI,90
@@ -576,7 +576,7 @@ reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM
576
576
  reconcile/utils/unleash.py,sha256=1D56CsZfE3ShDtN3IErE1T2eeIwNmxhK-yYbCotJ99E,3601
577
577
  reconcile/utils/vault.py,sha256=S0eHqvZ9N3fya1E8YDaUffEvLk_fdtpzL4rvWn6f828,14991
578
578
  reconcile/utils/vaultsecretref.py,sha256=3Ed2uBy36TzSvL0B-l4FoWQqB2SbBKDKEuUPIO608Bo,931
579
- reconcile/utils/vcs.py,sha256=ez0XB_0vo6RefgYDmz5YMvLytXtWsbLDJ56ffgVIBXw,5461
579
+ reconcile/utils/vcs.py,sha256=o1r0n_IrU2El75CED_6sjR2GZGM-exuWsj5F7jONaMU,6779
580
580
  reconcile/utils/cloud_resource_best_practice/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
581
581
  reconcile/utils/cloud_resource_best_practice/aws_rds.py,sha256=EvE6XKLsrZ531MJptKqPht2lOETrOjySTHXk6CzMgo0,2279
582
582
  reconcile/utils/glitchtip/__init__.py,sha256=FT6iBhGqoe7KExFdbgL8AYUb64iW_4snF5__Dcl7yt0,258
@@ -653,8 +653,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
653
653
  tools/test/test_qontract_cli.py,sha256=d18KrdhtUGqoC7_kWZU128U0-VJEj-0rjFkLVufcI6I,2755
654
654
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
655
655
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
656
- qontract_reconcile-0.10.1rc495.dist-info/METADATA,sha256=IASjtuTraWNInmz6oRgiIuZvIsTJLO9_vNcbr6Fqa44,2349
657
- qontract_reconcile-0.10.1rc495.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
658
- qontract_reconcile-0.10.1rc495.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
659
- qontract_reconcile-0.10.1rc495.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
660
- qontract_reconcile-0.10.1rc495.dist-info/RECORD,,
656
+ qontract_reconcile-0.10.1rc497.dist-info/METADATA,sha256=IvyDujhOGuhQNFmCJDWvjg78ZPUpVBwcJwKlcVVWBlQ,2349
657
+ qontract_reconcile-0.10.1rc497.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
658
+ qontract_reconcile-0.10.1rc497.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
659
+ qontract_reconcile-0.10.1rc497.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
660
+ qontract_reconcile-0.10.1rc497.dist-info/RECORD,,
@@ -81,7 +81,6 @@ class SaasAutoPromotionsManager:
81
81
  self._fetch_publisher_real_world_states()
82
82
  self._compute_desired_subscriber_states()
83
83
  subscribers_with_diff = self._get_subscribers_with_diff()
84
- self._merge_request_manager.fetch_sapm_managed_open_merge_requests()
85
84
  self._merge_request_manager.housekeeping()
86
85
  self._merge_request_manager.create_promotion_merge_requests(
87
86
  subscribers=subscribers_with_diff
@@ -13,6 +13,7 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request
13
13
  from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
14
14
  CHANNELS_REF,
15
15
  CONTENT_HASHES,
16
+ IS_BATCHABLE,
16
17
  PROMOTION_DATA_SEPARATOR,
17
18
  SAPM_LABEL,
18
19
  SAPM_VERSION,
@@ -20,14 +21,18 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer impor
20
21
  Renderer,
21
22
  )
22
23
  from reconcile.saas_auto_promotions_manager.subscriber import Subscriber
23
- from reconcile.utils.vcs import VCS
24
+ from reconcile.utils.vcs import VCS, MRCheckStatus
25
+
26
+ ITEM_SEPARATOR = ","
24
27
 
25
28
 
26
29
  @dataclass
27
30
  class OpenMergeRequest:
28
31
  raw: ProjectMergeRequest
29
- content_hash: str
32
+ content_hashes: str
30
33
  channels: str
34
+ failed_mr_check: bool
35
+ is_batchable: bool
31
36
 
32
37
 
33
38
  class MergeRequestManager:
@@ -38,13 +43,10 @@ class MergeRequestManager:
38
43
 
39
44
  The idea is that for every channel combination there exists
40
45
  maximum one open MR in app-interface. I.e., all changes for
41
- a channel combination are batched in a single MR. A content hash in the
42
- description is used to identify the content of that MR. If a
43
- channel combination receives new content before an already open MR
44
- is merged, then the manager will first close the old MR and then
45
- open a new MR with the new content. We might need to change this
46
- batching approach in the future to have even less promotion MRs,
47
- but for now this is sufficient.
46
+ a channel combination at a minumum are batched in a single MR.
47
+ The MR description is used to store current state for SAPM.
48
+ Channels can also be batched together into a single MR to reduce the overall
49
+ amount of MRs.
48
50
  """
49
51
 
50
52
  def __init__(self, vcs: VCS, renderer: Renderer):
@@ -53,6 +55,7 @@ class MergeRequestManager:
53
55
  self._version_ref_regex = re.compile(rf"{VERSION_REF}: (.*)$", re.MULTILINE)
54
56
  self._content_hash_regex = re.compile(rf"{CONTENT_HASHES}: (.*)$", re.MULTILINE)
55
57
  self._channels_regex = re.compile(rf"{CHANNELS_REF}: (.*)$", re.MULTILINE)
58
+ self._is_batchable_regex = re.compile(rf"{IS_BATCHABLE}: (.*)$", re.MULTILINE)
56
59
  self._open_mrs: list[OpenMergeRequest] = []
57
60
  self._open_mrs_with_problems: list[OpenMergeRequest] = []
58
61
  self._open_raw_mrs: list[ProjectMergeRequest] = []
@@ -66,21 +69,22 @@ class MergeRequestManager:
66
69
  return ""
67
70
  return groups[0]
68
71
 
69
- def fetch_sapm_managed_open_merge_requests(self) -> None:
72
+ def _fetch_sapm_managed_open_merge_requests(self) -> None:
70
73
  all_open_mrs = self._vcs.get_open_app_interface_merge_requests()
71
74
  self._open_raw_mrs = [
72
75
  mr for mr in all_open_mrs if SAPM_LABEL in mr.attributes.get("labels")
73
76
  ]
74
77
 
75
- def housekeeping(self) -> None:
78
+ def _parse_raw_mrs(self) -> None:
76
79
  """
77
- Close bad MRs:
78
- - bad description format
79
- - old SAPM version
80
- - merge conflict
81
-
82
- --> if we bump the SAPM version, we automatically close
83
- old open MRs and replace them with new ones.
80
+ We store state in MR descriptions.
81
+ This function parses the state and stores a list of valid, parsed open MRs (current state).
82
+ If any issue is encountered during parsing, we consider this MR
83
+ to be broken and close it. Information we want to parse includes:
84
+ - SAPM_VERSION -> Close if it doesnt match current version
85
+ - CHANNELS
86
+ - CONTENT_HASHES
87
+ - IS_BATCHABLE flag
84
88
  """
85
89
  seen: set[tuple[str, str, str]] = set()
86
90
  for mr in self._open_raw_mrs:
@@ -134,10 +138,10 @@ class MergeRequestManager:
134
138
  )
135
139
  continue
136
140
 
137
- content_hash = self._apply_regex(
141
+ content_hashes = self._apply_regex(
138
142
  pattern=self._content_hash_regex, promotion_data=promotion_data
139
143
  )
140
- if not content_hash:
144
+ if not content_hashes:
141
145
  logging.info(
142
146
  "Bad %s format. Closing %s",
143
147
  CONTENT_HASHES,
@@ -148,10 +152,10 @@ class MergeRequestManager:
148
152
  )
149
153
  continue
150
154
 
151
- channels_ref = self._apply_regex(
155
+ channels_refs = self._apply_regex(
152
156
  pattern=self._channels_regex, promotion_data=promotion_data
153
157
  )
154
- if not channels_ref:
158
+ if not channels_refs:
155
159
  logging.info(
156
160
  "Bad %s format. Closing %s",
157
161
  CHANNELS_REF,
@@ -162,7 +166,7 @@ class MergeRequestManager:
162
166
  )
163
167
  continue
164
168
 
165
- key = (version_ref, channels_ref, content_hash)
169
+ key = (version_ref, channels_refs, content_hashes)
166
170
  if key in seen:
167
171
  logging.info(
168
172
  "Duplicate MR detected. Closing %s",
@@ -175,14 +179,49 @@ class MergeRequestManager:
175
179
  continue
176
180
  seen.add(key)
177
181
 
182
+ is_batchable_str = self._apply_regex(
183
+ pattern=self._is_batchable_regex, promotion_data=promotion_data
184
+ )
185
+ if is_batchable_str not in set(["True", "False"]):
186
+ logging.info(
187
+ "Bad %s format. Closing %s",
188
+ IS_BATCHABLE,
189
+ mr.attributes.get("web_url", "NO_WEBURL"),
190
+ )
191
+ self._vcs.close_app_interface_mr(
192
+ mr, f"Closing this MR because of bad {IS_BATCHABLE} format."
193
+ )
194
+ continue
195
+
196
+ mr_check_status = self._vcs.get_gitlab_mr_check_status(mr)
197
+
178
198
  self._open_mrs.append(
179
199
  OpenMergeRequest(
180
200
  raw=mr,
181
- content_hash=content_hash,
182
- channels=channels_ref,
201
+ content_hashes=content_hashes,
202
+ channels=channels_refs,
203
+ failed_mr_check=mr_check_status == MRCheckStatus.FAILED,
204
+ is_batchable=bool(is_batchable_str),
183
205
  )
184
206
  )
185
207
 
208
+ def _unbatch_failed_mrs(self) -> None:
209
+ """
210
+ We optimistically batch MRs together that didnt run through MR check yet.
211
+ Vast majority of auto-promotion MRs are succeeding checks, so we can stay optimistic.
212
+ In the rare case of an MR failing the check, we want to unbatch it.
213
+ I.e., we open a dedicated MR for each channel in the batched MR, mark the new MRs as non-batchable
214
+ and close the old batched MR. By doing so, we ensure that unrelated MRs are not blocking each other.
215
+ Unbatched MRs are marked and will never be batched again.
216
+ """
217
+ # TODO: implemented in follow-up MR
218
+ pass
219
+
220
+ def housekeeping(self) -> None:
221
+ self._fetch_sapm_managed_open_merge_requests()
222
+ self._parse_raw_mrs()
223
+ self._unbatch_failed_mrs()
224
+
186
225
  def _aggregate_subscribers_per_channel_combo(
187
226
  self, subscribers: Iterable[Subscriber]
188
227
  ) -> dict[str, list[Subscriber]]:
@@ -196,7 +235,7 @@ class MergeRequestManager:
196
235
  return any(
197
236
  True
198
237
  for mr in self._open_mrs
199
- if content_hash in mr.content_hash and channels in mr.channels
238
+ if content_hash in mr.content_hashes and channels in mr.channels
200
239
  )
201
240
 
202
241
  def create_promotion_merge_requests(
@@ -223,7 +262,7 @@ class MergeRequestManager:
223
262
  for mr in self._open_mrs:
224
263
  if channel_combo not in mr.channels:
225
264
  continue
226
- if combined_content_hash not in mr.content_hash:
265
+ if combined_content_hash not in mr.content_hashes:
227
266
  logging.info(
228
267
  "Closing MR %s because it has out-dated content",
229
268
  mr.raw.attributes.get("web_url", "NO_WEBURL"),
@@ -261,8 +300,9 @@ class MergeRequestManager:
261
300
  continue
262
301
 
263
302
  description = self._renderer.render_description(
264
- content_hash=combined_content_hash,
303
+ content_hashes=combined_content_hash,
265
304
  channels=channel_combo,
305
+ is_batchable=True,
266
306
  )
267
307
  title = self._renderer.render_title(channels=channel_combo)
268
308
  logging.info(
@@ -22,6 +22,7 @@ SAPM_VERSION = "1.1.0"
22
22
  SAPM_LABEL = "SAPM"
23
23
  CONTENT_HASHES = "content_hashes"
24
24
  CHANNELS_REF = "channels"
25
+ IS_BATCHABLE = "is_batchable"
25
26
  VERSION_REF = "sapm_version"
26
27
  SAPM_DESC = f"""
27
28
  This is an auto-promotion triggered by app-interface's [saas-auto-promotions-manager](https://github.com/app-sre/qontract-reconcile/tree/master/reconcile/saas_auto_promotions_manager) (SAPM).
@@ -134,7 +135,9 @@ class Renderer:
134
135
  new_content += stream.getvalue() or ""
135
136
  return new_content
136
137
 
137
- def render_description(self, content_hash: str, channels: str) -> str:
138
+ def render_description(
139
+ self, content_hashes: str, channels: str, is_batchable: bool
140
+ ) -> str:
138
141
  return f"""
139
142
  {SAPM_DESC}
140
143
 
@@ -142,7 +145,9 @@ class Renderer:
142
145
 
143
146
  {CHANNELS_REF}: {channels}
144
147
 
145
- {CONTENT_HASHES}: {content_hash}
148
+ {CONTENT_HASHES}: {content_hashes}
149
+
150
+ {IS_BATCHABLE}: {is_batchable}
146
151
 
147
152
  {VERSION_REF}: {SAPM_VERSION}
148
153
  """
@@ -13,6 +13,7 @@ from reconcile.gql_definitions.fragments.saas_target_namespace import (
13
13
  from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
14
14
  CHANNELS_REF,
15
15
  CONTENT_HASHES,
16
+ IS_BATCHABLE,
16
17
  PROMOTION_DATA_SEPARATOR,
17
18
  SAPM_LABEL,
18
19
  SAPM_VERSION,
@@ -30,6 +31,7 @@ from .data_keys import (
30
31
  HAS_CONFLICTS,
31
32
  LABELS,
32
33
  OPEN_MERGE_REQUESTS,
34
+ SUBSCRIBER_BATCHABLE,
33
35
  SUBSCRIBER_CHANNELS,
34
36
  SUBSCRIBER_CONTENT_HASH,
35
37
  SUBSCRIBER_DESIRED_CONFIG_HASHES,
@@ -52,6 +54,7 @@ def mr_builder() -> Callable[[Mapping], ProjectMergeRequest]:
52
54
  {VERSION_REF}: {data.get(VERSION_REF, SAPM_VERSION)}
53
55
  {CHANNELS_REF}: {data.get(SUBSCRIBER_CHANNELS, "some_channel")}
54
56
  {CONTENT_HASHES}: {data.get(SUBSCRIBER_CONTENT_HASH, "content_hash")}
57
+ {IS_BATCHABLE}: {data.get(SUBSCRIBER_BATCHABLE, "True")}
55
58
  """,
56
59
  "web_url": "http://localhost",
57
60
  "has_conflicts": False,
@@ -10,5 +10,6 @@ SUBSCRIBER_TARGET_PATH = "target_path"
10
10
  SUBSCRIBER_DESIRED_CONFIG_HASHES = "config_hashes"
11
11
  SUBSCRIBER_DESIRED_REF = "desired_ref"
12
12
  SUBSCRIBER_CHANNELS = "sub_channels"
13
+ SUBSCRIBER_BATCHABLE = "sub_batchable"
13
14
  HAS_CONFLICTS = "has_conflicts"
14
15
  SUBSCRIBER_TARGET_NAMESPACE = "subscriber_target_namespace"
@@ -53,7 +53,6 @@ def test_close_old_content(
53
53
  vcs=vcs,
54
54
  renderer=renderer,
55
55
  )
56
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
57
56
  merge_request_manager.housekeeping()
58
57
  merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
59
58
 
@@ -112,7 +111,6 @@ def test_merge_request_already_opened(
112
111
  vcs=vcs,
113
112
  renderer=renderer,
114
113
  )
115
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
116
114
  merge_request_manager.housekeeping()
117
115
  merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
118
116
 
@@ -170,7 +168,6 @@ def test_ignore_unrelated_channels(
170
168
  vcs=vcs,
171
169
  renderer=renderer,
172
170
  )
173
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
174
171
  merge_request_manager.housekeeping()
175
172
  merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
176
173
 
@@ -9,6 +9,7 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_
9
9
  from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
10
10
  CHANNELS_REF,
11
11
  CONTENT_HASHES,
12
+ IS_BATCHABLE,
12
13
  PROMOTION_DATA_SEPARATOR,
13
14
  SAPM_LABEL,
14
15
  SAPM_VERSION,
@@ -25,16 +26,30 @@ from .data_keys import (
25
26
  )
26
27
 
27
28
 
28
- def test_labels_filter(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
29
+ def test_labels_filter(
30
+ vcs_builder: Callable[[Mapping], VCS], renderer: Renderer
31
+ ) -> None:
29
32
  vcs = vcs_builder({
30
33
  OPEN_MERGE_REQUESTS: [
31
34
  {
32
35
  LABELS: ["OtherLabel"],
33
- DESCRIPTION: "Some desc",
36
+ DESCRIPTION: f"""
37
+ Blabla
38
+ {PROMOTION_DATA_SEPARATOR}
39
+ {VERSION_REF}: {SAPM_VERSION}
40
+ {CHANNELS_REF}: some-channel
41
+ {CONTENT_HASHES}: some_hash
42
+ """,
34
43
  },
35
44
  {
36
45
  LABELS: [SAPM_LABEL, "OtherLabel"],
37
- DESCRIPTION: "Some desc",
46
+ DESCRIPTION: f"""
47
+ Blabla
48
+ {PROMOTION_DATA_SEPARATOR}
49
+ {VERSION_REF}: {SAPM_VERSION}
50
+ {CHANNELS_REF}: other-channel
51
+ {CONTENT_HASHES}: other_hash
52
+ """,
38
53
  },
39
54
  ]
40
55
  })
@@ -42,11 +57,13 @@ def test_labels_filter(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer
42
57
  vcs=vcs,
43
58
  renderer=renderer,
44
59
  )
45
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
60
+ merge_request_manager.housekeeping()
46
61
  assert len(merge_request_manager._open_raw_mrs) == 1
47
62
 
48
63
 
49
- def test_valid_description(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
64
+ def test_valid_description(
65
+ vcs_builder: Callable[[Mapping], VCS], renderer: Renderer
66
+ ) -> None:
50
67
  vcs = vcs_builder({
51
68
  OPEN_MERGE_REQUESTS: [
52
69
  {
@@ -57,6 +74,7 @@ def test_valid_description(vcs_builder: Callable[[Mapping], VCS], renderer: Rend
57
74
  {VERSION_REF}: {SAPM_VERSION}
58
75
  {CHANNELS_REF}: some-channel
59
76
  {CONTENT_HASHES}: some_hash
77
+ {IS_BATCHABLE}: True
60
78
  """,
61
79
  }
62
80
  ]
@@ -65,13 +83,50 @@ def test_valid_description(vcs_builder: Callable[[Mapping], VCS], renderer: Rend
65
83
  vcs=vcs,
66
84
  renderer=renderer,
67
85
  )
68
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
69
86
  merge_request_manager.housekeeping()
70
87
  vcs.close_app_interface_mr.assert_not_called() # type: ignore[attr-defined]
71
88
  assert len(merge_request_manager._open_mrs) == 1
72
89
 
73
90
 
74
- def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
91
+ def test_valid_batching(
92
+ vcs_builder: Callable[[Mapping], VCS], renderer: Renderer
93
+ ) -> None:
94
+ vcs = vcs_builder({
95
+ OPEN_MERGE_REQUESTS: [
96
+ {
97
+ LABELS: [SAPM_LABEL],
98
+ DESCRIPTION: f"""
99
+ Blabla
100
+ {PROMOTION_DATA_SEPARATOR}
101
+ {VERSION_REF}: {SAPM_VERSION}
102
+ {CHANNELS_REF}: some-channel
103
+ {CONTENT_HASHES}: some_hash
104
+ {IS_BATCHABLE}: False
105
+ """,
106
+ },
107
+ {
108
+ LABELS: [SAPM_LABEL],
109
+ DESCRIPTION: f"""
110
+ Blabla
111
+ {PROMOTION_DATA_SEPARATOR}
112
+ {VERSION_REF}: {SAPM_VERSION}
113
+ {CHANNELS_REF}: other-channel
114
+ {CONTENT_HASHES}: other_hash
115
+ {IS_BATCHABLE}: True
116
+ """,
117
+ },
118
+ ]
119
+ })
120
+ merge_request_manager = MergeRequestManager(
121
+ vcs=vcs,
122
+ renderer=renderer,
123
+ )
124
+ merge_request_manager.housekeeping()
125
+ vcs.close_app_interface_mr.assert_not_called() # type: ignore[attr-defined]
126
+ assert len(merge_request_manager._open_mrs) == 2
127
+
128
+
129
+ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer) -> None:
75
130
  vcs = vcs_builder({
76
131
  OPEN_MERGE_REQUESTS: [
77
132
  {
@@ -82,6 +137,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
82
137
  missing-version: some_version
83
138
  {CHANNELS_REF}: some-channel
84
139
  {CONTENT_HASHES}: hash_1
140
+ {IS_BATCHABLE}: True
85
141
  """,
86
142
  },
87
143
  {
@@ -91,6 +147,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
91
147
  {PROMOTION_DATA_SEPARATOR}
92
148
  {VERSION_REF}: {SAPM_VERSION}
93
149
  {CHANNELS_REF}: some-channel
150
+ {IS_BATCHABLE}: True
94
151
  missing-content-hash-key: some_hash
95
152
  """,
96
153
  },
@@ -101,6 +158,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
101
158
  missing-data-separator
102
159
  {VERSION_REF}: {SAPM_VERSION}
103
160
  {CONTENT_HASHES}: hash_3
161
+ {IS_BATCHABLE}: True
104
162
  """,
105
163
  },
106
164
  {
@@ -111,6 +169,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
111
169
  {PROMOTION_DATA_SEPARATOR}
112
170
  {CHANNELS_REF}: some-channel
113
171
  {CONTENT_HASHES}: hash_4
172
+ {IS_BATCHABLE}: True
114
173
  """,
115
174
  },
116
175
  {
@@ -123,6 +182,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
123
182
  {VERSION_REF}: {SAPM_VERSION}
124
183
  {CHANNELS_REF}: some-channel
125
184
  {CONTENT_HASHES}: hash_5
185
+ {IS_BATCHABLE}: True
126
186
  """,
127
187
  },
128
188
  {
@@ -133,6 +193,7 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
133
193
  {VERSION_REF}: outdated-version
134
194
  {CHANNELS_REF}: some-channel
135
195
  {CONTENT_HASHES}: hash_6
196
+ {IS_BATCHABLE}: True
136
197
  """,
137
198
  },
138
199
  {
@@ -143,6 +204,29 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
143
204
  {VERSION_REF}: {SAPM_VERSION}
144
205
  bad_channel_ref: some-channel
145
206
  {CONTENT_HASHES}: hash_7
207
+ {IS_BATCHABLE}: True
208
+ """,
209
+ },
210
+ {
211
+ LABELS: [SAPM_LABEL],
212
+ DESCRIPTION: f"""
213
+ Blabla
214
+ {PROMOTION_DATA_SEPARATOR}
215
+ {VERSION_REF}: {SAPM_VERSION}
216
+ {CHANNELS_REF}: some-channel
217
+ {CONTENT_HASHES}: hash_8
218
+ missing-batchable-key
219
+ """,
220
+ },
221
+ {
222
+ LABELS: [SAPM_LABEL],
223
+ DESCRIPTION: f"""
224
+ Blabla
225
+ {PROMOTION_DATA_SEPARATOR}
226
+ {VERSION_REF}: {SAPM_VERSION}
227
+ {CHANNELS_REF}: some-channel
228
+ {CONTENT_HASHES}: hash_9
229
+ {IS_BATCHABLE}: Something-non-bool
146
230
  """,
147
231
  },
148
232
  ]
@@ -151,13 +235,14 @@ def test_bad_mrs(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
151
235
  vcs=vcs,
152
236
  renderer=renderer,
153
237
  )
154
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
155
238
  merge_request_manager.housekeeping()
156
239
  vcs.close_app_interface_mr.assert_called() # type: ignore[attr-defined]
157
240
  assert len(merge_request_manager._open_mrs) == 0
158
241
 
159
242
 
160
- def test_remove_duplicates(vcs_builder: Callable[[Mapping], VCS], renderer: Renderer):
243
+ def test_remove_duplicates(
244
+ vcs_builder: Callable[[Mapping], VCS], renderer: Renderer
245
+ ) -> None:
161
246
  vcs = vcs_builder({
162
247
  OPEN_MERGE_REQUESTS: [
163
248
  {
@@ -168,6 +253,7 @@ def test_remove_duplicates(vcs_builder: Callable[[Mapping], VCS], renderer: Rend
168
253
  {VERSION_REF}: {SAPM_VERSION}
169
254
  {CHANNELS_REF}: some_channel
170
255
  {CONTENT_HASHES}: same_hash
256
+ {IS_BATCHABLE}: True
171
257
  """,
172
258
  },
173
259
  {
@@ -178,6 +264,7 @@ def test_remove_duplicates(vcs_builder: Callable[[Mapping], VCS], renderer: Rend
178
264
  {VERSION_REF}: {SAPM_VERSION}
179
265
  {CHANNELS_REF}: some_channel
180
266
  {CONTENT_HASHES}: same_hash
267
+ {IS_BATCHABLE}: True
181
268
  """,
182
269
  },
183
270
  ]
@@ -186,7 +273,6 @@ def test_remove_duplicates(vcs_builder: Callable[[Mapping], VCS], renderer: Rend
186
273
  vcs=vcs,
187
274
  renderer=renderer,
188
275
  )
189
- merge_request_manager.fetch_sapm_managed_open_merge_requests()
190
276
  merge_request_manager.housekeeping()
191
277
  vcs.close_app_interface_mr.assert_called_once() # type: ignore[attr-defined]
192
278
  assert len(merge_request_manager._open_mrs) == 1
reconcile/utils/vcs.py CHANGED
@@ -1,6 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import re
3
5
  from collections.abc import Iterable
6
+ from enum import Enum
4
7
  from typing import Optional
5
8
 
6
9
  from gitlab.v4.objects import ProjectMergeRequest
@@ -19,6 +22,13 @@ from reconcile.utils.secret_reader import (
19
22
  )
20
23
 
21
24
 
25
+ class MRCheckStatus(Enum):
26
+ NONE = 0
27
+ SUCCESS = 1
28
+ FAILED = 2
29
+ RUNNING = 3
30
+
31
+
22
32
  class VCS:
23
33
  """
24
34
  Abstraction layer for aggregating different Version Control Systems.
@@ -38,17 +48,32 @@ class VCS:
38
48
  dry_run: bool,
39
49
  allow_deleting_mrs: bool,
40
50
  allow_opening_mrs: bool,
51
+ gitlab_instance: Optional[GitLabApi] = None,
52
+ default_gh_token: Optional[str] = None,
53
+ app_interface_api: Optional[GitLabApi] = None,
41
54
  ):
42
55
  self._dry_run = dry_run
43
56
  self._allow_deleting_mrs = allow_deleting_mrs
44
57
  self._allow_opening_mrs = allow_opening_mrs
45
58
  self._secret_reader = secret_reader
46
59
  self._gh_per_repo_url: dict[str, GithubRepositoryApi] = {}
47
- self._default_gh_token = self._get_default_gh_token(github_orgs=github_orgs)
48
- self._gitlab_instance = self._gitlab_api(gitlab_instances=gitlab_instances)
49
- self._app_interface_api = self._init_app_interface_api(
50
- gitlab_instances=gitlab_instances,
51
- app_interface_repo_url=app_interface_repo_url,
60
+ self._default_gh_token = (
61
+ default_gh_token
62
+ if default_gh_token
63
+ else self._get_default_gh_token(github_orgs=github_orgs)
64
+ )
65
+ self._gitlab_instance = (
66
+ gitlab_instance
67
+ if gitlab_instance
68
+ else self._gitlab_api(gitlab_instances=gitlab_instances)
69
+ )
70
+ self._app_interface_api = (
71
+ app_interface_api
72
+ if app_interface_api
73
+ else self._init_app_interface_api(
74
+ gitlab_instances=gitlab_instances,
75
+ app_interface_repo_url=app_interface_repo_url,
76
+ )
52
77
  )
53
78
  self._is_commit_sha_regex = re.compile(r"^[0-9a-f]{40}$")
54
79
 
@@ -104,6 +129,23 @@ class VCS:
104
129
  project_url=app_interface_repo_url,
105
130
  )
106
131
 
132
+ def get_gitlab_mr_check_status(self, mr: ProjectMergeRequest) -> MRCheckStatus:
133
+ pipelines = self._gitlab_instance.get_merge_request_pipelines(mr)
134
+ if not pipelines:
135
+ return MRCheckStatus.NONE
136
+ # available status codes https://docs.gitlab.com/ee/api/pipelines.html
137
+ last_pipeline_result = pipelines[0]["status"]
138
+ match last_pipeline_result:
139
+ case "success":
140
+ return MRCheckStatus.SUCCESS
141
+ case "running":
142
+ return MRCheckStatus.RUNNING
143
+ case "failed":
144
+ return MRCheckStatus.FAILED
145
+ case _:
146
+ # Lets assume all other states as non-present
147
+ return MRCheckStatus.NONE
148
+
107
149
  def get_commit_sha(
108
150
  self, repo_url: str, ref: str, auth_code: Optional[HasSecret]
109
151
  ) -> str: