qontract-reconcile 0.10.1rc514__py3-none-any.whl → 0.10.1rc515__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.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/RECORD +17 -15
- reconcile/saas_auto_promotions_manager/integration.py +28 -3
- reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py +5 -5
- reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager_v2.py +144 -0
- reconcile/saas_auto_promotions_manager/merge_request_manager/mr_parser.py +13 -9
- reconcile/saas_auto_promotions_manager/merge_request_manager/reconciler.py +207 -0
- reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +9 -6
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +25 -12
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py +3 -0
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py +51 -153
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_mr_parser.py +11 -9
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_reconciler.py +408 -0
- reconcile/test/saas_auto_promotions_manager/test_integration_test.py +23 -88
- reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_unbatching.py +0 -96
- {qontract_reconcile-0.10.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc514.dist-info → qontract_reconcile-0.10.1rc515.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.1rc515
|
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.1rc514.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/RECORD
RENAMED
@@ -345,14 +345,16 @@ reconcile/rhidp/sso_client/base.py,sha256=EfQ2ewcOKh5idg46UKAkY6z0m_nGQfvnQKffa2
|
|
345
345
|
reconcile/rhidp/sso_client/integration.py,sha256=kA8g7c38ZBSdrRtyfEqy_WgSreD1PbwY7ZIN-3tZRPc,2221
|
346
346
|
reconcile/rhidp/sso_client/metrics.py,sha256=Tq7tSOsqL3XdcPUdozxqzSPIodUeOV87UCTqpuuqqhw,1013
|
347
347
|
reconcile/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
348
|
-
reconcile/saas_auto_promotions_manager/integration.py,sha256=
|
348
|
+
reconcile/saas_auto_promotions_manager/integration.py,sha256=1nm3aV5a0frEPTSww4ue1mfQ_Px6XaRZoaBRhPgJiAU,6472
|
349
349
|
reconcile/saas_auto_promotions_manager/publisher.py,sha256=4_M9Oykhj-kEZPUn05E2DY5gD6-x32Dgf7K3NPOkGEg,2029
|
350
350
|
reconcile/saas_auto_promotions_manager/subscriber.py,sha256=cLhPlkT71J2LIice3SLmH1WpsqzV46gd0peMxrnqyRw,7452
|
351
351
|
reconcile/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
352
352
|
reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request.py,sha256=PNu7sE-tDUY61E03z5w0b93fdowZ8auCl0S4_vhYOKQ,1333
|
353
|
-
reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py,sha256=
|
354
|
-
reconcile/saas_auto_promotions_manager/merge_request_manager/
|
355
|
-
reconcile/saas_auto_promotions_manager/merge_request_manager/
|
353
|
+
reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py,sha256=P4RH6y-qC9RKJxVguyOfOAe42a6AP1Ez-NlyMSH3j8I,7492
|
354
|
+
reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager_v2.py,sha256=AwCyvnNAKwRbESodHN2-fWG8Wpxua0nzQkcIsz4Gevg,5411
|
355
|
+
reconcile/saas_auto_promotions_manager/merge_request_manager/mr_parser.py,sha256=x8Gg-YjEFWEeDPJH3Y8SrfcJbwhLuAqCz4kIhfEyaaA,7060
|
356
|
+
reconcile/saas_auto_promotions_manager/merge_request_manager/reconciler.py,sha256=DgMw24exjuor9s4Y8y0Ftsjs5l7rl8D7dksMoJ1RF0A,7639
|
357
|
+
reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py,sha256=VSKCwY8jZ6NYwIT6edmVnpzY7cgjhn5fhX9xtlw2LD8,7341
|
356
358
|
reconcile/saas_auto_promotions_manager/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
357
359
|
reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py,sha256=mihuWynroB1Cea1Lsvf6V8Nb8PGiBdcLC0uhCX_52Y0,6966
|
358
360
|
reconcile/skupper_network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -452,14 +454,14 @@ reconcile/test/test_vpc_peerings_validator.py,sha256=dFSmjc_dMN2GqMbntCFpa7PUZmy
|
|
452
454
|
reconcile/test/test_wrong_region.py,sha256=7KzL7OaICQ9Z3DW27zt_ykMN7_87owAFC-2CYjvGoyA,2138
|
453
455
|
reconcile/test/saas_auto_promotions_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
454
456
|
reconcile/test/saas_auto_promotions_manager/conftest.py,sha256=tZNs35EuWulP53Cqt61RteOGY_uH4gfhtXfGQ8-awwM,3081
|
455
|
-
reconcile/test/saas_auto_promotions_manager/test_integration_test.py,sha256=
|
457
|
+
reconcile/test/saas_auto_promotions_manager/test_integration_test.py,sha256=UIafVpeof9Zd3Q17mEDIUwgdBGf8oFxkf7h8tLfmwCE,2077
|
456
458
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
457
459
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
458
|
-
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py,sha256=
|
459
|
-
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py,sha256=
|
460
|
-
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py,sha256=
|
461
|
-
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_mr_parser.py,sha256=
|
462
|
-
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/
|
460
|
+
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py,sha256=7O3lbk1EmEtUofqGncfiwMYvDPXrkQNPB59zlQ_zXkM,4588
|
461
|
+
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py,sha256=Z1IV51OUuzhd-3S8W-k7ixC-fkaglCokn0eakK0Z73s,606
|
462
|
+
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py,sha256=ryROiiQNtZvuG820CB-RQ9FMmLAGshezyMDNDiJNA_E,2369
|
463
|
+
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_mr_parser.py,sha256=dcGHzxuafKSxmswSO1qF2WlKaqsmEvtERC6Lb8kDAN0,10019
|
464
|
+
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_reconciler.py,sha256=-rSv-C-JFaY0gsjSFgcXz9ASNQQOzwizaQEbVYJtLHg,14906
|
463
465
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
464
466
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py,sha256=2rCSstewp4LPoEJHm5N7dGJexEtY8ndLHvoGZYjmpsc,1678
|
465
467
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/data_keys.py,sha256=beHYQ9kgDLeBZgC2FvxQA3tHx1PO-RAMN8_kVcSdikI,90
|
@@ -666,8 +668,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
666
668
|
tools/test/test_qontract_cli.py,sha256=d18KrdhtUGqoC7_kWZU128U0-VJEj-0rjFkLVufcI6I,2755
|
667
669
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
668
670
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
669
|
-
qontract_reconcile-0.10.
|
670
|
-
qontract_reconcile-0.10.
|
671
|
-
qontract_reconcile-0.10.
|
672
|
-
qontract_reconcile-0.10.
|
673
|
-
qontract_reconcile-0.10.
|
671
|
+
qontract_reconcile-0.10.1rc515.dist-info/METADATA,sha256=Bgwt_oRZlcfCQ6ycYlopR_xt-fRVu3kWrlOW7wMuaqo,2349
|
672
|
+
qontract_reconcile-0.10.1rc515.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
673
|
+
qontract_reconcile-0.10.1rc515.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
|
674
|
+
qontract_reconcile-0.10.1rc515.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
675
|
+
qontract_reconcile-0.10.1rc515.dist-info/RECORD,,
|
@@ -9,9 +9,15 @@ from reconcile.openshift_saas_deploy import (
|
|
9
9
|
from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager import (
|
10
10
|
MergeRequestManager,
|
11
11
|
)
|
12
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager_v2 import (
|
13
|
+
MergeRequestManagerV2,
|
14
|
+
)
|
12
15
|
from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
|
13
16
|
MRParser,
|
14
17
|
)
|
18
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.reconciler import (
|
19
|
+
Reconciler,
|
20
|
+
)
|
15
21
|
from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
|
16
22
|
Renderer,
|
17
23
|
)
|
@@ -45,6 +51,7 @@ class SaasAutoPromotionsManager:
|
|
45
51
|
deployment_state: PromotionState,
|
46
52
|
vcs: VCS,
|
47
53
|
saas_file_inventory: SaasFilesInventory,
|
54
|
+
merge_request_manager_v2: MergeRequestManagerV2,
|
48
55
|
merge_request_manager: MergeRequestManager,
|
49
56
|
thread_pool_size: int,
|
50
57
|
dry_run: bool,
|
@@ -52,6 +59,7 @@ class SaasAutoPromotionsManager:
|
|
52
59
|
self._deployment_state = deployment_state
|
53
60
|
self._vcs = vcs
|
54
61
|
self._saas_file_inventory = saas_file_inventory
|
62
|
+
self._merge_request_manager_v2 = merge_request_manager_v2
|
55
63
|
self._merge_request_manager = merge_request_manager
|
56
64
|
self._thread_pool_size = thread_pool_size
|
57
65
|
self._dry_run = dry_run
|
@@ -88,11 +96,14 @@ class SaasAutoPromotionsManager:
|
|
88
96
|
self._merge_request_manager.create_promotion_merge_requests(
|
89
97
|
subscribers=subscribers_with_diff
|
90
98
|
)
|
99
|
+
self._merge_request_manager_v2.reconcile(subscribers=subscribers_with_diff)
|
91
100
|
|
92
101
|
|
93
102
|
def init_external_dependencies(
|
94
103
|
dry_run: bool,
|
95
|
-
) -> tuple[
|
104
|
+
) -> tuple[
|
105
|
+
PromotionState, VCS, SaasFilesInventory, MergeRequestManagerV2, MergeRequestManager
|
106
|
+
]:
|
96
107
|
"""
|
97
108
|
Lets initialize everything that involves calls to external dependencies:
|
98
109
|
- VCS -> Gitlab / Github queries
|
@@ -120,17 +131,29 @@ def init_external_dependencies(
|
|
120
131
|
allow_opening_mrs=allow_opening_mrs,
|
121
132
|
)
|
122
133
|
mr_parser = MRParser(vcs=vcs)
|
123
|
-
|
134
|
+
merge_request_manager_v2 = MergeRequestManagerV2(
|
124
135
|
vcs=vcs,
|
136
|
+
reconciler=Reconciler(),
|
125
137
|
mr_parser=mr_parser,
|
126
138
|
renderer=Renderer(),
|
127
139
|
)
|
140
|
+
merge_request_manager = MergeRequestManager(
|
141
|
+
mr_parser=mr_parser,
|
142
|
+
renderer=Renderer(),
|
143
|
+
vcs=vcs,
|
144
|
+
)
|
128
145
|
saas_files = get_saas_files()
|
129
146
|
saas_inventory = SaasFilesInventory(saas_files=saas_files)
|
130
147
|
deployment_state = PromotionState(
|
131
148
|
state=init_state(integration=OPENSHIFT_SAAS_DEPLOY, secret_reader=secret_reader)
|
132
149
|
)
|
133
|
-
return
|
150
|
+
return (
|
151
|
+
deployment_state,
|
152
|
+
vcs,
|
153
|
+
saas_inventory,
|
154
|
+
merge_request_manager_v2,
|
155
|
+
merge_request_manager,
|
156
|
+
)
|
134
157
|
|
135
158
|
|
136
159
|
@defer
|
@@ -143,6 +166,7 @@ def run(
|
|
143
166
|
deployment_state,
|
144
167
|
vcs,
|
145
168
|
saas_inventory,
|
169
|
+
merge_request_manager_v2,
|
146
170
|
merge_request_manager,
|
147
171
|
) = init_external_dependencies(dry_run=dry_run)
|
148
172
|
if defer:
|
@@ -153,6 +177,7 @@ def run(
|
|
153
177
|
vcs=vcs,
|
154
178
|
saas_file_inventory=saas_inventory,
|
155
179
|
merge_request_manager=merge_request_manager,
|
180
|
+
merge_request_manager_v2=merge_request_manager_v2,
|
156
181
|
thread_pool_size=thread_pool_size,
|
157
182
|
dry_run=dry_run,
|
158
183
|
)
|
@@ -16,7 +16,6 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer impor
|
|
16
16
|
CHANNELS_REF,
|
17
17
|
CONTENT_HASHES,
|
18
18
|
IS_BATCHABLE,
|
19
|
-
SAPM_LABEL,
|
20
19
|
VERSION_REF,
|
21
20
|
Renderer,
|
22
21
|
)
|
@@ -25,6 +24,8 @@ from reconcile.utils.vcs import VCS
|
|
25
24
|
|
26
25
|
ITEM_SEPARATOR = ","
|
27
26
|
|
27
|
+
SAPM_LABEL = "SAPM"
|
28
|
+
|
28
29
|
|
29
30
|
class MergeRequestManager:
|
30
31
|
"""
|
@@ -68,14 +69,13 @@ class MergeRequestManager:
|
|
68
69
|
"Closing this MR because it failed MR check and isn't marked un-batchable yet.",
|
69
70
|
)
|
70
71
|
# Remember these hashes as unbatchable
|
71
|
-
|
72
|
-
self._unbatchable_hashes.update(content_hashes)
|
72
|
+
self._unbatchable_hashes.update(mr.content_hashes)
|
73
73
|
else:
|
74
74
|
open_mrs_after_unbatching.append(mr)
|
75
75
|
self._open_mrs = open_mrs_after_unbatching
|
76
76
|
|
77
77
|
def housekeeping(self) -> None:
|
78
|
-
self._open_mrs = self._mr_parser.retrieve_open_mrs()
|
78
|
+
self._open_mrs = self._mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
|
79
79
|
self._unbatch_failed_mrs()
|
80
80
|
|
81
81
|
def _aggregate_subscribers_per_channel_combo(
|
@@ -160,7 +160,7 @@ class MergeRequestManager:
|
|
160
160
|
channels=channel_combo,
|
161
161
|
is_batchable=combined_content_hash not in self._unbatchable_hashes,
|
162
162
|
)
|
163
|
-
title = self._renderer.render_title(channels=channel_combo)
|
163
|
+
title = self._renderer.render_title(is_draft=False, channels=channel_combo)
|
164
164
|
logging.info(
|
165
165
|
"Open MR for update in channel(s) %s",
|
166
166
|
channel_combo,
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import logging
|
2
|
+
from collections import defaultdict
|
3
|
+
from collections.abc import Iterable
|
4
|
+
|
5
|
+
from gitlab.exceptions import GitlabGetError
|
6
|
+
|
7
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request import (
|
8
|
+
SAPMMR,
|
9
|
+
)
|
10
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
|
11
|
+
MRParser,
|
12
|
+
)
|
13
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.reconciler import (
|
14
|
+
Addition,
|
15
|
+
Promotion,
|
16
|
+
Reconciler,
|
17
|
+
)
|
18
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
|
19
|
+
Renderer,
|
20
|
+
)
|
21
|
+
from reconcile.saas_auto_promotions_manager.subscriber import Subscriber
|
22
|
+
from reconcile.utils.vcs import VCS
|
23
|
+
|
24
|
+
BATCH_SIZE_LIMIT = 5
|
25
|
+
SAPM_LABEL = "SAPMCanary"
|
26
|
+
|
27
|
+
|
28
|
+
class MergeRequestManagerV2:
|
29
|
+
"""
|
30
|
+
Manager for SAPM merge requests.
|
31
|
+
|
32
|
+
This class uses MRParser to fetch current state (currently open MRs).
|
33
|
+
This class calculates the desired state (i.e., desired promotions).
|
34
|
+
Desired state and current state are given to the Reconciler, which will
|
35
|
+
then determine a Diff (Additions and Deletions of MRs).
|
36
|
+
|
37
|
+
This class interacts with VCS to realize the result of the Diff.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self, vcs: VCS, mr_parser: MRParser, reconciler: Reconciler, renderer: Renderer
|
42
|
+
):
|
43
|
+
self._vcs = vcs
|
44
|
+
self._mr_parser = mr_parser
|
45
|
+
self._renderer = renderer
|
46
|
+
self._reconciler = reconciler
|
47
|
+
self._content_hash_to_subscriber: dict[str, list[Subscriber]] = {}
|
48
|
+
self._sapm_mrs: list[SAPMMR] = []
|
49
|
+
|
50
|
+
def _aggregate_desired_state(
|
51
|
+
self, subscribers: Iterable[Subscriber]
|
52
|
+
) -> list[Promotion]:
|
53
|
+
subscribers_per_channel_combo: dict[str, list[Subscriber]] = defaultdict(list)
|
54
|
+
for subscriber in subscribers:
|
55
|
+
channel_combo = ",".join([c.name for c in subscriber.channels])
|
56
|
+
subscribers_per_channel_combo[channel_combo].append(subscriber)
|
57
|
+
|
58
|
+
desired_promotions: list[Promotion] = []
|
59
|
+
for channel_combo, subs in subscribers_per_channel_combo.items():
|
60
|
+
combined_content_hash = Subscriber.combined_content_hash(subscribers=subs)
|
61
|
+
self._content_hash_to_subscriber[combined_content_hash] = subs
|
62
|
+
desired_promotions.append(
|
63
|
+
Promotion(
|
64
|
+
content_hashes={combined_content_hash},
|
65
|
+
channels={channel_combo},
|
66
|
+
)
|
67
|
+
)
|
68
|
+
return desired_promotions
|
69
|
+
|
70
|
+
def _render_mr(self, addition: Addition) -> None:
|
71
|
+
subs: list[Subscriber] = []
|
72
|
+
for content_hash in addition.content_hashes:
|
73
|
+
subs.extend(self._content_hash_to_subscriber[content_hash])
|
74
|
+
content_by_path: dict[str, str] = {}
|
75
|
+
has_error = False
|
76
|
+
for sub in subs:
|
77
|
+
if sub.target_file_path not in content_by_path:
|
78
|
+
try:
|
79
|
+
content_by_path[sub.target_file_path] = (
|
80
|
+
self._vcs.get_file_content_from_app_interface_master(
|
81
|
+
file_path=sub.target_file_path
|
82
|
+
)
|
83
|
+
)
|
84
|
+
except GitlabGetError as e:
|
85
|
+
if e.response_code == 404:
|
86
|
+
logging.error(
|
87
|
+
"The saas file %s does not exist anylonger. Most likely qontract-server data not in synch. This should resolve soon on its own.",
|
88
|
+
sub.target_file_path,
|
89
|
+
)
|
90
|
+
has_error = True
|
91
|
+
break
|
92
|
+
raise e
|
93
|
+
content_by_path[sub.target_file_path] = (
|
94
|
+
self._renderer.render_merge_request_content(
|
95
|
+
subscriber=sub,
|
96
|
+
current_content=content_by_path[sub.target_file_path],
|
97
|
+
)
|
98
|
+
)
|
99
|
+
if has_error:
|
100
|
+
return
|
101
|
+
|
102
|
+
description_hashes = ",".join(addition.content_hashes)
|
103
|
+
description_channels = ",".join(addition.channels)
|
104
|
+
|
105
|
+
description = self._renderer.render_description(
|
106
|
+
content_hashes=description_hashes,
|
107
|
+
channels=description_channels,
|
108
|
+
is_batchable=addition.batchable,
|
109
|
+
)
|
110
|
+
title = self._renderer.render_title(
|
111
|
+
is_draft=True, channels=description_channels
|
112
|
+
)
|
113
|
+
logging.info(
|
114
|
+
"Open MR for update in channel(s) %s",
|
115
|
+
description_channels,
|
116
|
+
)
|
117
|
+
self._sapm_mrs.append(
|
118
|
+
SAPMMR(
|
119
|
+
sapm_label=SAPM_LABEL,
|
120
|
+
content_by_path=content_by_path,
|
121
|
+
title=title,
|
122
|
+
description=description,
|
123
|
+
)
|
124
|
+
)
|
125
|
+
|
126
|
+
def reconcile(self, subscribers: Iterable[Subscriber]) -> None:
|
127
|
+
current_state = self._mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
|
128
|
+
desired_state = self._aggregate_desired_state(subscribers=subscribers)
|
129
|
+
diff = self._reconciler.reconcile(
|
130
|
+
batch_limit=BATCH_SIZE_LIMIT,
|
131
|
+
desired_promotions=desired_state,
|
132
|
+
open_mrs=current_state,
|
133
|
+
)
|
134
|
+
for deletion in diff.deletions:
|
135
|
+
self._vcs.close_app_interface_mr(
|
136
|
+
mr=deletion.mr.raw,
|
137
|
+
comment=deletion.reason,
|
138
|
+
)
|
139
|
+
|
140
|
+
for addition in diff.additions:
|
141
|
+
self._render_mr(addition=addition)
|
142
|
+
|
143
|
+
for rendered_mr in self._sapm_mrs:
|
144
|
+
self._vcs.open_app_interface_merge_request(mr=rendered_mr)
|
@@ -9,7 +9,6 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer impor
|
|
9
9
|
CONTENT_HASHES,
|
10
10
|
IS_BATCHABLE,
|
11
11
|
PROMOTION_DATA_SEPARATOR,
|
12
|
-
SAPM_LABEL,
|
13
12
|
SAPM_VERSION,
|
14
13
|
VERSION_REF,
|
15
14
|
)
|
@@ -19,12 +18,15 @@ from reconcile.utils.vcs import VCS, MRCheckStatus
|
|
19
18
|
@dataclass
|
20
19
|
class OpenMergeRequest:
|
21
20
|
raw: ProjectMergeRequest
|
22
|
-
content_hashes: str
|
23
|
-
channels: str
|
21
|
+
content_hashes: set[str]
|
22
|
+
channels: set[str]
|
24
23
|
failed_mr_check: bool
|
25
24
|
is_batchable: bool
|
26
25
|
|
27
26
|
|
27
|
+
ITEM_SEPARATOR = ","
|
28
|
+
|
29
|
+
|
28
30
|
class MRParser:
|
29
31
|
"""
|
30
32
|
We store state in MR descriptions.
|
@@ -49,11 +51,13 @@ class MRParser:
|
|
49
51
|
return ""
|
50
52
|
return groups[0]
|
51
53
|
|
52
|
-
def _fetch_sapm_managed_open_merge_requests(
|
54
|
+
def _fetch_sapm_managed_open_merge_requests(
|
55
|
+
self, label: str
|
56
|
+
) -> list[ProjectMergeRequest]:
|
53
57
|
all_open_mrs = self._vcs.get_open_app_interface_merge_requests()
|
54
|
-
return [mr for mr in all_open_mrs if
|
58
|
+
return [mr for mr in all_open_mrs if label in mr.attributes.get("labels")]
|
55
59
|
|
56
|
-
def retrieve_open_mrs(self) -> list[OpenMergeRequest]:
|
60
|
+
def retrieve_open_mrs(self, label: str) -> list[OpenMergeRequest]:
|
57
61
|
"""
|
58
62
|
This function parses the state and returns a list of valid, parsed open MRs (current state).
|
59
63
|
If any issue is encountered during parsing, we consider this MR
|
@@ -66,7 +70,7 @@ class MRParser:
|
|
66
70
|
"""
|
67
71
|
open_mrs: list[OpenMergeRequest] = []
|
68
72
|
seen: set[tuple[str, str, str]] = set()
|
69
|
-
for mr in self._fetch_sapm_managed_open_merge_requests():
|
73
|
+
for mr in self._fetch_sapm_managed_open_merge_requests(label=label):
|
70
74
|
attrs = mr.attributes
|
71
75
|
desc = attrs.get("description")
|
72
76
|
has_conflicts = attrs.get("has_conflicts", False)
|
@@ -177,8 +181,8 @@ class MRParser:
|
|
177
181
|
open_mrs.append(
|
178
182
|
OpenMergeRequest(
|
179
183
|
raw=mr,
|
180
|
-
content_hashes=content_hashes,
|
181
|
-
channels=channels_refs,
|
184
|
+
content_hashes=set(content_hashes.split(ITEM_SEPARATOR)),
|
185
|
+
channels=set(channels_refs.split(ITEM_SEPARATOR)),
|
182
186
|
failed_mr_check=mr_check_status == MRCheckStatus.FAILED,
|
183
187
|
is_batchable=is_batchable_str == "True",
|
184
188
|
)
|
@@ -0,0 +1,207 @@
|
|
1
|
+
from collections.abc import Iterable
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
|
6
|
+
OpenMergeRequest,
|
7
|
+
)
|
8
|
+
|
9
|
+
MSG_MISSING_UNBATCHING = (
|
10
|
+
"Closing this MR because it failed MR check and isn't marked as un-batchable yet."
|
11
|
+
)
|
12
|
+
MSG_OUTDATED_CONTENT = "Closing this MR because it has out-dated content."
|
13
|
+
MSG_NEW_BATCH = "Closing this MR in favor of a new batch MR."
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class Promotion:
|
18
|
+
content_hashes: set[str]
|
19
|
+
channels: set[str]
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class Deletion:
|
24
|
+
mr: OpenMergeRequest
|
25
|
+
reason: str
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class Addition:
|
30
|
+
content_hashes: set[str]
|
31
|
+
channels: set[str]
|
32
|
+
batchable: bool
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class Diff:
|
37
|
+
deletions: list[Deletion]
|
38
|
+
additions: list[Addition]
|
39
|
+
|
40
|
+
|
41
|
+
class Reconciler:
|
42
|
+
"""
|
43
|
+
The reconciler calculates a Diff. I.e., which MRs need to be opened (Addition)
|
44
|
+
and which MRs need to be closed (Deletion). The reconciler has no external
|
45
|
+
dependencies and does not interact with VCS. The reconciler expects to be
|
46
|
+
given the desired state (which promotions do we want) and the current state
|
47
|
+
(the currently open MRs) in order to calculate the Diff.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self) -> None:
|
51
|
+
self._desired_promotions: Iterable[Promotion] = []
|
52
|
+
self._open_mrs: Iterable[OpenMergeRequest] = []
|
53
|
+
|
54
|
+
def _unbatch(self, diff: Diff) -> None:
|
55
|
+
"""
|
56
|
+
We optimistically batch MRs together that didnt run through MR check yet.
|
57
|
+
Vast majority of auto-promotion MRs are succeeding checks, so we can stay optimistic.
|
58
|
+
In the rare case of an MR failing the check, we want to unbatch it.
|
59
|
+
I.e., we open a dedicated MR for each channel in the batched MR, mark the new MRs as non-batchable
|
60
|
+
and close the old batched MR. By doing so, we ensure that unrelated MRs are not blocking each other.
|
61
|
+
Unbatched MRs are marked and will never be batched again.
|
62
|
+
"""
|
63
|
+
open_mrs_after_unbatching: list[OpenMergeRequest] = []
|
64
|
+
unbatchable_hashes: set[str] = set()
|
65
|
+
falsely_marked_batchable_hashes: set[str] = set()
|
66
|
+
for mr in self._open_mrs:
|
67
|
+
if not mr.is_batchable:
|
68
|
+
unbatchable_hashes.update(mr.content_hashes)
|
69
|
+
open_mrs_after_unbatching.append(mr)
|
70
|
+
elif mr.failed_mr_check:
|
71
|
+
falsely_marked_batchable_hashes.update(mr.content_hashes)
|
72
|
+
diff.deletions.append(
|
73
|
+
Deletion(
|
74
|
+
mr=mr,
|
75
|
+
reason=MSG_MISSING_UNBATCHING,
|
76
|
+
)
|
77
|
+
)
|
78
|
+
else:
|
79
|
+
open_mrs_after_unbatching.append(mr)
|
80
|
+
self._open_mrs = open_mrs_after_unbatching
|
81
|
+
|
82
|
+
desired_promotions_after_unbatching: list[Promotion] = []
|
83
|
+
for promotion in self._desired_promotions:
|
84
|
+
if promotion.content_hashes.issubset(unbatchable_hashes):
|
85
|
+
continue
|
86
|
+
elif promotion.content_hashes.issubset(falsely_marked_batchable_hashes):
|
87
|
+
diff.additions.append(
|
88
|
+
Addition(
|
89
|
+
content_hashes=promotion.content_hashes,
|
90
|
+
channels=promotion.channels,
|
91
|
+
batchable=False,
|
92
|
+
)
|
93
|
+
)
|
94
|
+
continue
|
95
|
+
else:
|
96
|
+
desired_promotions_after_unbatching.append(promotion)
|
97
|
+
self._desired_promotions = desired_promotions_after_unbatching
|
98
|
+
|
99
|
+
def _remove_outdated(self, diff: Diff) -> None:
|
100
|
+
"""
|
101
|
+
We want to be sure that the open MRs are still addressing desired content.
|
102
|
+
We close MRs that are not addressing any content hash in a desired promotion.
|
103
|
+
"""
|
104
|
+
all_desired_content_hashes: set[str] = set()
|
105
|
+
for promotion in self._desired_promotions:
|
106
|
+
all_desired_content_hashes.update(promotion.content_hashes)
|
107
|
+
|
108
|
+
open_mrs_after_deletion: list[OpenMergeRequest] = []
|
109
|
+
for mr in self._open_mrs:
|
110
|
+
if mr.content_hashes.issubset(all_desired_content_hashes):
|
111
|
+
open_mrs_after_deletion.append(mr)
|
112
|
+
continue
|
113
|
+
diff.deletions.append(
|
114
|
+
Deletion(
|
115
|
+
mr=mr,
|
116
|
+
reason=MSG_OUTDATED_CONTENT,
|
117
|
+
)
|
118
|
+
)
|
119
|
+
self._open_mrs = open_mrs_after_deletion
|
120
|
+
|
121
|
+
def _batch_remaining_mrs(self, diff: Diff, batch_limit: int) -> None:
|
122
|
+
"""
|
123
|
+
Remaining desired promotions should be batched together
|
124
|
+
if they are not addressed yet by a valid open MR.
|
125
|
+
|
126
|
+
We do not want to let MR batches grow infinitely,
|
127
|
+
as constant change of a batch might starve MRs that
|
128
|
+
are already part of the batch.
|
129
|
+
In order for MRs to not grow infinitely, we apply a
|
130
|
+
BATCH_LIMIT per MR, i.e., a maximum number of hashes
|
131
|
+
that can be batched together.
|
132
|
+
"""
|
133
|
+
submitted_content_hashes: set[str] = set()
|
134
|
+
for open_mr in self._open_mrs:
|
135
|
+
submitted_content_hashes.update(open_mr.content_hashes)
|
136
|
+
|
137
|
+
unsubmitted_promotions = [
|
138
|
+
prom
|
139
|
+
for prom in self._desired_promotions
|
140
|
+
if not prom.content_hashes.issubset(submitted_content_hashes)
|
141
|
+
]
|
142
|
+
|
143
|
+
if not unsubmitted_promotions:
|
144
|
+
return
|
145
|
+
|
146
|
+
batch_with_capacity: Optional[OpenMergeRequest] = None
|
147
|
+
for mr in self._open_mrs:
|
148
|
+
if len(mr.content_hashes) < batch_limit:
|
149
|
+
batch_with_capacity = mr
|
150
|
+
# Note, there should always only be maximum one batch with capacity available
|
151
|
+
break
|
152
|
+
|
153
|
+
if batch_with_capacity:
|
154
|
+
# We disassemble the batch to its promotions
|
155
|
+
# can be added to new batch(es)
|
156
|
+
unsubmitted_promotions.append(
|
157
|
+
Promotion(
|
158
|
+
content_hashes=batch_with_capacity.content_hashes,
|
159
|
+
channels=batch_with_capacity.channels,
|
160
|
+
)
|
161
|
+
)
|
162
|
+
# Lets close the current batch so remaining promotions can
|
163
|
+
# be aggregated in new batch(es)
|
164
|
+
diff.deletions.append(
|
165
|
+
Deletion(
|
166
|
+
mr=batch_with_capacity,
|
167
|
+
reason=MSG_NEW_BATCH,
|
168
|
+
)
|
169
|
+
)
|
170
|
+
|
171
|
+
batched_mr = Addition(
|
172
|
+
content_hashes=set(),
|
173
|
+
channels=set(),
|
174
|
+
batchable=True,
|
175
|
+
)
|
176
|
+
|
177
|
+
for promotion in unsubmitted_promotions:
|
178
|
+
batched_mr.content_hashes.update(promotion.content_hashes)
|
179
|
+
batched_mr.channels.update(promotion.channels)
|
180
|
+
if len(batched_mr.content_hashes) >= batch_limit:
|
181
|
+
# Note, we might also be above the batch limit, but thats ok.
|
182
|
+
# We only ensure that we create a new batch now and dont grow further.
|
183
|
+
diff.additions.append(batched_mr)
|
184
|
+
batched_mr = Addition(
|
185
|
+
content_hashes=set(),
|
186
|
+
channels=set(),
|
187
|
+
batchable=True,
|
188
|
+
)
|
189
|
+
if batched_mr.content_hashes:
|
190
|
+
diff.additions.append(batched_mr)
|
191
|
+
|
192
|
+
def reconcile(
|
193
|
+
self,
|
194
|
+
desired_promotions: Iterable[Promotion],
|
195
|
+
open_mrs: Iterable[OpenMergeRequest],
|
196
|
+
batch_limit: int,
|
197
|
+
) -> Diff:
|
198
|
+
self._open_mrs = open_mrs
|
199
|
+
self._desired_promotions = desired_promotions
|
200
|
+
diff = Diff(
|
201
|
+
deletions=[],
|
202
|
+
additions=[],
|
203
|
+
)
|
204
|
+
self._unbatch(diff=diff)
|
205
|
+
self._remove_outdated(diff=diff)
|
206
|
+
self._batch_remaining_mrs(diff=diff, batch_limit=batch_limit)
|
207
|
+
return diff
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import hashlib
|
1
2
|
from collections.abc import Mapping
|
2
3
|
from copy import deepcopy
|
3
4
|
from typing import Any
|
@@ -18,18 +19,17 @@ from reconcile.saas_auto_promotions_manager.subscriber import Subscriber
|
|
18
19
|
PROMOTION_DATA_SEPARATOR = (
|
19
20
|
"**SAPM Data - DO NOT MANUALLY CHANGE ANYTHING BELOW THIS LINE**"
|
20
21
|
)
|
21
|
-
SAPM_VERSION = "
|
22
|
-
SAPM_LABEL = "SAPM"
|
22
|
+
SAPM_VERSION = "2.0.0"
|
23
23
|
CONTENT_HASHES = "content_hashes"
|
24
24
|
CHANNELS_REF = "channels"
|
25
25
|
IS_BATCHABLE = "is_batchable"
|
26
26
|
VERSION_REF = "sapm_version"
|
27
|
-
SAPM_DESC =
|
27
|
+
SAPM_DESC = """
|
28
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).
|
29
29
|
The channel(s) mentioned in the MR title had an event.
|
30
30
|
This MR promotes all subscribers with auto-promotions for these channel(s).
|
31
31
|
|
32
|
-
Please **do not remove
|
32
|
+
Please **do not remove or change any label** from this MR.
|
33
33
|
|
34
34
|
Parts of this description are used by SAPM to manage auto-promotions.
|
35
35
|
"""
|
@@ -152,8 +152,11 @@ class Renderer:
|
|
152
152
|
{VERSION_REF}: {SAPM_VERSION}
|
153
153
|
"""
|
154
154
|
|
155
|
-
def render_title(self, channels: str) -> str:
|
156
|
-
|
155
|
+
def render_title(self, is_draft: bool, channels: str) -> str:
|
156
|
+
content = f"[SAPM] auto-promotion ID {int(hashlib.sha256(channels.encode('utf-8')).hexdigest(), 16) % 10**8}"
|
157
|
+
if is_draft:
|
158
|
+
return f"Draft: {content}"
|
159
|
+
return content
|
157
160
|
|
158
161
|
|
159
162
|
def _parse_expression(expression: str) -> Any:
|