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.
- {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/RECORD +19 -17
- reconcile/cli.py +3 -3
- reconcile/{template_tester.py → resource_template_tester.py} +3 -3
- 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.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc513.dist-info → qontract_reconcile-0.10.1rc515.dist-info}/entry_points.txt +0 -0
- {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 = "
|
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:
|
reconcile/test/saas_auto_promotions_manager/merge_request_manager/merge_request_manager/conftest.py
CHANGED
@@ -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[[
|
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 =
|
124
|
-
subscriber.desired_ref = data.get(
|
125
|
-
for channel in data.get(
|
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,
|
@@ -1,185 +1,83 @@
|
|
1
|
-
from collections.abc import
|
2
|
-
|
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.
|
12
|
-
|
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.
|
23
|
-
|
24
|
-
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
104
|
-
|
105
|
-
open_mrs = [
|
106
|
-
OpenMergeRequest(
|
43
|
+
deletion = Deletion(
|
44
|
+
mr=OpenMergeRequest(
|
107
45
|
raw=create_autospec(spec=ProjectMergeRequest),
|
108
|
-
|
109
|
-
|
110
|
-
is_batchable=True,
|
46
|
+
channels=set(),
|
47
|
+
content_hashes=set(),
|
111
48
|
failed_mr_check=False,
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
174
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
vcs.
|
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],
|