qontract-reconcile 0.10.1rc513__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.
Files changed (20) hide show
  1. {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/RECORD +19 -17
  3. reconcile/cli.py +3 -3
  4. reconcile/{template_tester.py → resource_template_tester.py} +3 -3
  5. reconcile/saas_auto_promotions_manager/integration.py +28 -3
  6. reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager.py +5 -5
  7. reconcile/saas_auto_promotions_manager/merge_request_manager/merge_request_manager_v2.py +144 -0
  8. reconcile/saas_auto_promotions_manager/merge_request_manager/mr_parser.py +13 -9
  9. reconcile/saas_auto_promotions_manager/merge_request_manager/reconciler.py +207 -0
  10. reconcile/saas_auto_promotions_manager/merge_request_manager/renderer.py +9 -6
  11. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py +25 -12
  12. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/data_keys.py +3 -0
  13. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_merge_request_manager.py +51 -153
  14. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_mr_parser.py +11 -9
  15. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_reconciler.py +408 -0
  16. reconcile/test/saas_auto_promotions_manager/test_integration_test.py +23 -88
  17. reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/test_unbatching.py +0 -96
  18. {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/WHEEL +0 -0
  19. {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/entry_points.txt +0 -0
  20. {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/top_level.txt +0 -0
@@ -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 = "1.1.0"
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 = f"""
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 the {SAPM_LABEL} label** from this MR.
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
- return f"[SAPM] auto-promotion for {channels}"
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:
@@ -11,16 +11,22 @@ from gitlab.v4.objects import ProjectMergeRequest
11
11
  from reconcile.gql_definitions.fragments.saas_target_namespace import (
12
12
  SaasTargetNamespace,
13
13
  )
14
+ from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager_v2 import (
15
+ SAPM_LABEL,
16
+ )
14
17
  from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
15
18
  MRParser,
16
19
  OpenMergeRequest,
17
20
  )
21
+ from reconcile.saas_auto_promotions_manager.merge_request_manager.reconciler import (
22
+ Diff,
23
+ Reconciler,
24
+ )
18
25
  from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
19
26
  CHANNELS_REF,
20
27
  CONTENT_HASHES,
21
28
  IS_BATCHABLE,
22
29
  PROMOTION_DATA_SEPARATOR,
23
- SAPM_LABEL,
24
30
  SAPM_VERSION,
25
31
  VERSION_REF,
26
32
  Renderer,
@@ -32,17 +38,16 @@ from reconcile.saas_auto_promotions_manager.subscriber import (
32
38
  from reconcile.utils.vcs import VCS, MRCheckStatus
33
39
 
34
40
  from .data_keys import (
41
+ CHANNEL,
35
42
  DESCRIPTION,
36
43
  HAS_CONFLICTS,
37
44
  LABELS,
38
45
  OPEN_MERGE_REQUESTS,
39
46
  PIPELINE_RESULTS,
47
+ REF,
40
48
  SUBSCRIBER_BATCHABLE,
41
49
  SUBSCRIBER_CHANNELS,
42
50
  SUBSCRIBER_CONTENT_HASH,
43
- SUBSCRIBER_DESIRED_CONFIG_HASHES,
44
- SUBSCRIBER_DESIRED_REF,
45
- SUBSCRIBER_TARGET_NAMESPACE,
46
51
  SUBSCRIBER_TARGET_PATH,
47
52
  )
48
53
 
@@ -96,7 +101,7 @@ def vcs_builder(
96
101
 
97
102
 
98
103
  @pytest.fixture
99
- def mr_parser_builder() -> Callable[[Mapping], MRParser]:
104
+ def mr_parser_builder() -> Callable[[Iterable[OpenMergeRequest]], MRParser]:
100
105
  def builder(data: Iterable[OpenMergeRequest]) -> MRParser:
101
106
  mr_parser = create_autospec(spec=MRParser)
102
107
  mr_parser.retrieve_open_mrs.side_effect = [data]
@@ -105,24 +110,32 @@ def mr_parser_builder() -> Callable[[Mapping], MRParser]:
105
110
  return builder
106
111
 
107
112
 
113
+ @pytest.fixture
114
+ def reconciler_builder() -> Callable[[Diff], Reconciler]:
115
+ def builder(data: Diff) -> Reconciler:
116
+ reconciler = create_autospec(spec=Reconciler)
117
+ reconciler.reconcile.side_effect = [data]
118
+ return reconciler
119
+
120
+ return builder
121
+
122
+
108
123
  @pytest.fixture
109
124
  def subscriber_builder(
110
125
  saas_target_namespace_builder: Callable[..., SaasTargetNamespace],
111
- ):
126
+ ) -> Callable[..., Subscriber]:
112
127
  def builder(data: Mapping) -> Subscriber:
113
128
  subscriber = Subscriber(
114
129
  saas_name="",
115
130
  template_name="",
116
- target_namespace=saas_target_namespace_builder(
117
- data.get(SUBSCRIBER_TARGET_NAMESPACE, {})
118
- ),
131
+ target_namespace=saas_target_namespace_builder({}),
119
132
  ref="",
120
133
  target_file_path=data.get(SUBSCRIBER_TARGET_PATH, ""),
121
134
  use_target_config_hash=True,
122
135
  )
123
- subscriber.desired_hashes = data.get(SUBSCRIBER_DESIRED_CONFIG_HASHES, [])
124
- subscriber.desired_ref = data.get(SUBSCRIBER_DESIRED_REF, "")
125
- for channel in data.get(SUBSCRIBER_CHANNELS, []):
136
+ subscriber.desired_hashes = []
137
+ subscriber.desired_ref = data.get(REF, "")
138
+ for channel in data.get(CHANNEL, []):
126
139
  subscriber.channels.append(
127
140
  Channel(
128
141
  name=channel,
@@ -14,3 +14,6 @@ SUBSCRIBER_BATCHABLE = "sub_batchable"
14
14
  HAS_CONFLICTS = "has_conflicts"
15
15
  SUBSCRIBER_TARGET_NAMESPACE = "subscriber_target_namespace"
16
16
  PIPELINE_RESULTS = "pipeline_results"
17
+
18
+ CHANNEL = "channel"
19
+ REF = "ref"
@@ -1,185 +1,83 @@
1
- from collections.abc import (
2
- Callable,
3
- Iterable,
4
- Mapping,
5
- )
6
- from unittest.mock import create_autospec
1
+ from collections.abc import Callable
2
+ from unittest.mock import call, create_autospec
7
3
 
8
- import pytest
9
4
  from gitlab.v4.objects import ProjectMergeRequest
10
5
 
11
- from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager import (
12
- MergeRequestManager,
6
+ from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager_v2 import (
7
+ MergeRequestManagerV2,
13
8
  )
14
9
  from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
15
10
  MRParser,
16
11
  OpenMergeRequest,
17
12
  )
13
+ from reconcile.saas_auto_promotions_manager.merge_request_manager.reconciler import (
14
+ Addition,
15
+ Deletion,
16
+ Diff,
17
+ Reconciler,
18
+ )
18
19
  from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer import (
19
20
  Renderer,
20
21
  )
21
22
  from reconcile.saas_auto_promotions_manager.subscriber import Subscriber
22
- from reconcile.utils.vcs import VCS
23
-
24
- from .data_keys import (
25
- SUBSCRIBER_CHANNELS,
26
- SUBSCRIBER_DESIRED_CONFIG_HASHES,
27
- SUBSCRIBER_DESIRED_REF,
28
- SUBSCRIBER_TARGET_NAMESPACE,
29
- SUBSCRIBER_TARGET_PATH,
23
+ from reconcile.test.saas_auto_promotions_manager.merge_request_manager.merge_request_manager.data_keys import (
24
+ CHANNEL,
25
+ REF,
30
26
  )
27
+ from reconcile.utils.vcs import VCS
31
28
 
32
29
 
33
- def test_close_old_content(
34
- mr_parser_builder: Callable[[Iterable], MRParser],
35
- renderer: Renderer,
36
- subscriber_builder: Callable[[Mapping], Subscriber],
37
- ):
38
- subscribers = [
39
- subscriber_builder({
40
- SUBSCRIBER_TARGET_NAMESPACE: {"path": "namespace1"},
41
- SUBSCRIBER_TARGET_PATH: "target1",
42
- SUBSCRIBER_DESIRED_REF: "new_sha",
43
- SUBSCRIBER_DESIRED_CONFIG_HASHES: [],
44
- SUBSCRIBER_CHANNELS: ["channel-a", "channel-b"],
45
- })
46
- ]
47
-
48
- open_mrs = [
49
- OpenMergeRequest(
50
- raw=create_autospec(ProjectMergeRequest),
51
- content_hashes="oldcontent",
52
- channels="channel-a,channel-b",
53
- failed_mr_check=False,
54
- is_batchable=True,
55
- )
56
- ]
57
- mr_parser = mr_parser_builder(open_mrs)
30
+ def test_reconcile(
31
+ reconciler_builder: Callable[[Diff], Reconciler],
32
+ subscriber_builder: Callable[..., Subscriber],
33
+ ) -> None:
58
34
  vcs = create_autospec(spec=VCS)
59
- merge_request_manager = MergeRequestManager(
60
- vcs=vcs,
61
- mr_parser=mr_parser,
62
- renderer=renderer,
63
- )
64
- merge_request_manager.housekeeping()
65
- merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
66
-
67
- # There is an open MR with old content for that subscriber
68
- # Close old content and open new MR with new content
69
- vcs.close_app_interface_mr.assert_called_once()
70
- vcs.open_app_interface_merge_request.assert_called_once()
71
-
72
-
73
- @pytest.mark.parametrize(
74
- "hash_prefix, hash_suffix, channel_prefix, channel_suffix",
75
- [
76
- ("", "", "", ""),
77
- ("hashprefix,", "", "", ""),
78
- ("", ",hashsuffix", "", ""),
79
- ("", "", "channelprefix,", ""),
80
- ("", "", "", ",channelsuffix"),
81
- ("a,", ",b", "c,", ",d"),
82
- ],
83
- )
84
- def test_merge_request_already_opened(
85
- mr_parser_builder: Callable[[Iterable], MRParser],
86
- renderer: Renderer,
87
- subscriber_builder: Callable[[Mapping], Subscriber],
88
- hash_prefix: str,
89
- hash_suffix: str,
90
- channel_prefix: str,
91
- channel_suffix: str,
92
- ):
93
- subscriber_channel = "channel-a"
35
+ mr_parser = create_autospec(spec=MRParser)
36
+ renderer = create_autospec(spec=Renderer)
94
37
  subscribers = [
95
38
  subscriber_builder({
96
- SUBSCRIBER_TARGET_NAMESPACE: {"path": "namespace1"},
97
- SUBSCRIBER_TARGET_PATH: "target1",
98
- SUBSCRIBER_DESIRED_REF: "new_sha",
99
- SUBSCRIBER_DESIRED_CONFIG_HASHES: [],
100
- SUBSCRIBER_CHANNELS: [subscriber_channel],
39
+ CHANNEL: ["chan1,chan2"],
40
+ REF: "hash1",
101
41
  })
102
42
  ]
103
- content_hash = Subscriber.combined_content_hash(subscribers=subscribers)
104
-
105
- open_mrs = [
106
- OpenMergeRequest(
43
+ deletion = Deletion(
44
+ mr=OpenMergeRequest(
107
45
  raw=create_autospec(spec=ProjectMergeRequest),
108
- content_hashes=f"{hash_prefix}{content_hash}{hash_suffix}",
109
- channels=f"{channel_prefix}{subscriber_channel}{channel_suffix}",
110
- is_batchable=True,
46
+ channels=set(),
47
+ content_hashes=set(),
111
48
  failed_mr_check=False,
112
- )
113
- ]
114
- mr_parser = mr_parser_builder(open_mrs)
115
-
116
- vcs = create_autospec(spec=VCS)
117
- merge_request_manager = MergeRequestManager(
118
- vcs=vcs,
119
- mr_parser=mr_parser,
120
- renderer=renderer,
49
+ is_batchable=True,
50
+ ),
51
+ reason="some reason.",
121
52
  )
122
- merge_request_manager.housekeeping()
123
- merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
124
-
125
- # There is already an open merge request for this subscriber content
126
- # Do not open another one
127
- vcs.close_app_interface_mr.assert_not_called()
128
- vcs.open_app_interface_merge_request.assert_not_called()
129
53
 
130
-
131
- @pytest.mark.parametrize(
132
- "hash_prefix, hash_suffix, channel_prefix, channel_suffix",
133
- [
134
- ("", "", "", ""),
135
- ("hashprefix,", "", "", ""),
136
- ("", ",hashsuffix", "", ""),
137
- ("", "", "channelprefix,", ""),
138
- ("", "", "", ",channelsuffix"),
139
- ("a,", ",b", "c,", ",d"),
140
- ],
141
- )
142
- def test_ignore_unrelated_channels(
143
- mr_parser_builder: Callable[[Iterable], MRParser],
144
- renderer: Renderer,
145
- subscriber_builder: Callable[[Mapping], Subscriber],
146
- hash_prefix: str,
147
- hash_suffix: str,
148
- channel_prefix: str,
149
- channel_suffix: str,
150
- ):
151
- subscribers = [
152
- subscriber_builder({
153
- SUBSCRIBER_TARGET_NAMESPACE: {"path": "namespace1"},
154
- SUBSCRIBER_TARGET_PATH: "target1",
155
- SUBSCRIBER_DESIRED_REF: "new_sha",
156
- SUBSCRIBER_DESIRED_CONFIG_HASHES: [],
157
- SUBSCRIBER_CHANNELS: ["channel-a"],
158
- })
159
- ]
160
- content_hash = Subscriber.combined_content_hash(subscribers=subscribers)
161
-
162
- open_mrs = [
163
- OpenMergeRequest(
164
- raw=create_autospec(spec=ProjectMergeRequest),
165
- content_hashes=f"{hash_prefix}{content_hash}{hash_suffix}",
166
- channels=f"{channel_prefix}other-channel{channel_suffix}",
167
- is_batchable=True,
168
- failed_mr_check=False,
54
+ additions = [
55
+ Addition(
56
+ content_hashes={Subscriber.combined_content_hash(subscribers=subscribers)},
57
+ channels={"chan1,chan2"},
58
+ batchable=True,
169
59
  )
170
60
  ]
171
- mr_parser = mr_parser_builder(open_mrs)
172
61
 
173
- vcs = create_autospec(spec=VCS)
174
- merge_request_manager = MergeRequestManager(
62
+ reconciler = reconciler_builder(
63
+ Diff(
64
+ deletions=[deletion],
65
+ additions=additions,
66
+ )
67
+ )
68
+ manager = MergeRequestManagerV2(
175
69
  vcs=vcs,
176
70
  mr_parser=mr_parser,
71
+ reconciler=reconciler,
177
72
  renderer=renderer,
178
73
  )
179
- merge_request_manager.housekeeping()
180
- merge_request_manager.create_promotion_merge_requests(subscribers=subscribers)
181
74
 
182
- # There is already an open merge request for this subscriber content
183
- # Do not open another one, because the channels do not match
184
- vcs.close_app_interface_mr.assert_not_called()
185
- vcs.open_app_interface_merge_request.assert_called_once()
75
+ manager.reconcile(subscribers=subscribers)
76
+
77
+ assert len(manager._sapm_mrs) == len(additions)
78
+ vcs.close_app_interface_mr.assert_has_calls([
79
+ call(deletion.mr.raw, deletion.reason),
80
+ ])
81
+ vcs.open_app_interface_merge_request.assert_has_calls([
82
+ call(mr) for mr in manager._sapm_mrs
83
+ ])
@@ -6,6 +6,9 @@ from unittest.mock import call
6
6
 
7
7
  from gitlab.v4.objects import ProjectMergeRequest
8
8
 
9
+ from reconcile.saas_auto_promotions_manager.merge_request_manager.merge_request_manager_v2 import (
10
+ SAPM_LABEL,
11
+ )
9
12
  from reconcile.saas_auto_promotions_manager.merge_request_manager.mr_parser import (
10
13
  MRParser,
11
14
  )
@@ -14,7 +17,6 @@ from reconcile.saas_auto_promotions_manager.merge_request_manager.renderer impor
14
17
  CONTENT_HASHES,
15
18
  IS_BATCHABLE,
16
19
  PROMOTION_DATA_SEPARATOR,
17
- SAPM_LABEL,
18
20
  SAPM_VERSION,
19
21
  VERSION_REF,
20
22
  )
@@ -60,17 +62,17 @@ def test_valid_parsing(
60
62
  mr_parser = MRParser(
61
63
  vcs=vcs,
62
64
  )
63
- open_mrs = mr_parser.retrieve_open_mrs()
65
+ open_mrs = mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
64
66
  assert len(open_mrs) == 2
65
67
 
66
68
  assert open_mrs[0].raw == expectd_mrs[0]
67
- assert open_mrs[0].channels == "channel0"
68
- assert open_mrs[0].content_hashes == "hash0"
69
+ assert open_mrs[0].channels == {"channel0"}
70
+ assert open_mrs[0].content_hashes == {"hash0"}
69
71
  assert open_mrs[0].is_batchable
70
72
 
71
73
  assert open_mrs[1].raw == expectd_mrs[1]
72
- assert open_mrs[1].channels == "channel1"
73
- assert open_mrs[1].content_hashes == "hash1"
74
+ assert open_mrs[1].channels == {"channel1"}
75
+ assert open_mrs[1].content_hashes == {"hash1"}
74
76
  assert not open_mrs[1].is_batchable
75
77
 
76
78
 
@@ -107,7 +109,7 @@ def test_labels_filter(
107
109
  mr_parser = MRParser(
108
110
  vcs=vcs,
109
111
  )
110
- open_mrs = mr_parser.retrieve_open_mrs()
112
+ open_mrs = mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
111
113
  assert len(open_mrs) == 1
112
114
  assert open_mrs[0].raw == expectd_mrs[0]
113
115
 
@@ -261,7 +263,7 @@ def test_bad_mrs(
261
263
  ),
262
264
  ]
263
265
 
264
- open_mrs = mr_parser.retrieve_open_mrs()
266
+ open_mrs = mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
265
267
  assert len(open_mrs) == 0
266
268
  vcs.close_app_interface_mr.assert_has_calls(expected_calls) # type: ignore[attr-defined]
267
269
  assert vcs.close_app_interface_mr.call_count == len(expected_calls) # type: ignore[attr-defined]
@@ -299,7 +301,7 @@ def test_remove_duplicates(
299
301
  mr_parser = MRParser(
300
302
  vcs=vcs,
301
303
  )
302
- open_mrs = mr_parser.retrieve_open_mrs()
304
+ open_mrs = mr_parser.retrieve_open_mrs(label=SAPM_LABEL)
303
305
  vcs.close_app_interface_mr.assert_has_calls([ # type: ignore[attr-defined]
304
306
  call(
305
307
  expected_mrs[1],