qontract-reconcile 0.10.1rc861__py3-none-any.whl → 0.10.1rc863__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.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/RECORD +10 -9
- reconcile/gitlab_permissions.py +23 -0
- reconcile/test/test_gitlab_permissions.py +57 -0
- reconcile/test/test_three_way_diff_strategy.py +10 -0
- reconcile/utils/gitlab_api.py +57 -10
- reconcile/utils/three_way_diff_strategy.py +8 -0
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.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.1rc863
|
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.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/RECORD
RENAMED
@@ -35,7 +35,7 @@ reconcile/gitlab_labeler.py,sha256=IxE1XM5o4rDOFuR4cM2yAHTy4Uzdg3Nyz2mp7b8Fx1g,4
|
|
35
35
|
reconcile/gitlab_members.py,sha256=M6LwFOrwgvl1NNdOJa1mrQFUon-bEVv1AyhGeLed454,8443
|
36
36
|
reconcile/gitlab_mr_sqs_consumer.py,sha256=O46mdziPgGOndbU-0_UJKJVUaiEoVzJPEgKm4_UvYoI,2571
|
37
37
|
reconcile/gitlab_owners.py,sha256=sn9njaKOtqcvnhi2qtm-faAfAR4zNqflbSuusA9RUuI,13456
|
38
|
-
reconcile/gitlab_permissions.py,sha256=
|
38
|
+
reconcile/gitlab_permissions.py,sha256=LcO5tVND3Rh-onU6WFh5ZWPCqZ-U4AVuFTZIs_bmPwA,3153
|
39
39
|
reconcile/gitlab_projects.py,sha256=K3tFf_aD1W4Ijp5q-9Qek3kwFGEWPcZ1kd7tzFJ4GyQ,1781
|
40
40
|
reconcile/integrations_manager.py,sha256=J_VV-HINI7YNav2NPIolePZkll-7VBuBXWAyMNhsM_Q,9535
|
41
41
|
reconcile/jenkins_base.py,sha256=0Gocu3fU2YTltaxBlbDQOUvP-7CP2OSQV1ZRwtWeVXw,875
|
@@ -501,6 +501,7 @@ reconcile/test/test_github_repo_invites.py,sha256=UVaDlxSxi5iooyUbz8F11d7cvINHLK
|
|
501
501
|
reconcile/test/test_gitlab_housekeeping.py,sha256=Sn5rERCp28sMiBx5vJaQ5yy80y37GouMClejmXocsT8,10068
|
502
502
|
reconcile/test/test_gitlab_labeler.py,sha256=vFLUJXSIaCduj6wSqgw7Fg7FhlopaDnYI5SLzNHtLoY,4362
|
503
503
|
reconcile/test/test_gitlab_members.py,sha256=kaCOA02eZSrSMkzHmaLwWW3LY6Af0ciLSEP4PlMLvOU,9949
|
504
|
+
reconcile/test/test_gitlab_permissions.py,sha256=FAKT7UuNAjxmke90P2cA-924_CGZ7Kp3JLTb6rmTseM,1965
|
504
505
|
reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
|
505
506
|
reconcile/test/test_integrations_manager.py,sha256=l6KwSFT0NS9VSR-b_9z_ZEGXDWH3EMitUEMC_1h8Xkk,38184
|
506
507
|
reconcile/test/test_jenkins_worker_fleets.py,sha256=o1jlT7OBBSgu0M3iI4xMdz_x6SciF7yhNBpLk5gTJfg,2361
|
@@ -550,7 +551,7 @@ reconcile/test/test_terraform_tgw_attachments.py,sha256=rHZHUtDxewpKsRj3nfm2bZ2J
|
|
550
551
|
reconcile/test/test_terraform_users.py,sha256=XOAfGvITCJPI1LTlISmHbA4ONMQMkxYUMTsny7pQCFw,4319
|
551
552
|
reconcile/test/test_terraform_vpc_peerings.py,sha256=Btl0ym7NmO2QFST9Xviz4OO1RjJuhCp1Xhix5A3e_HQ,20822
|
552
553
|
reconcile/test/test_terraform_vpc_peerings_build_desired_state.py,sha256=7VAFVbjlnnUJoOkZ4ApDc1lHFj38Zj4yrbDKvWkqWXE,49545
|
553
|
-
reconcile/test/test_three_way_diff_strategy.py,sha256=
|
554
|
+
reconcile/test/test_three_way_diff_strategy.py,sha256=v3rNkQFNy5e1uyfeNSlNBA07fvrPGD0aXD91Lgv8oxc,4062
|
554
555
|
reconcile/test/test_utils_jinja2.py,sha256=TpzQlpFnLGzNEZp5WOh0o7AuBiGEktqO4MuwiiJW2YY,3895
|
555
556
|
reconcile/test/test_vault_replication.py,sha256=wlc4jm9f8P641UvvxIFFFc5_unJysNkOVrKJscjhQr0,16867
|
556
557
|
reconcile/test/test_vault_utils.py,sha256=vbJnc89XAuE07qbTuWxHM5o9F6R9SO5aHXA38fwxT7A,1122
|
@@ -652,7 +653,7 @@ reconcile/utils/filtering.py,sha256=zZnHH0u0SaTDyzuFXZ_mREURGLvjEqQIQy4z-7QBVlc,
|
|
652
653
|
reconcile/utils/git.py,sha256=BdxXFgQ1XOZpS-4qb3qMsKTCFDG8MlE26rv1jAhvCkM,1560
|
653
654
|
reconcile/utils/git_secrets.py,sha256=0wGNL5mvDtVPRuu3vEQgld1Am64gIDJHtmu1_ZKxMAI,1973
|
654
655
|
reconcile/utils/github_api.py,sha256=_bttNxYKeam_tLVe27L7O4gKqSn6CeyuFnJn8tSaUVY,2488
|
655
|
-
reconcile/utils/gitlab_api.py,sha256=
|
656
|
+
reconcile/utils/gitlab_api.py,sha256=kGYNyx6pbF43rd6R3a3wcIGmATiqNUFJUUHP4sELgNI,29027
|
656
657
|
reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
|
657
658
|
reconcile/utils/gql.py,sha256=qMWj39ilJGV1YAeUvbqVYq6u9zxuPvZqxFLaLb6A-bA,14256
|
658
659
|
reconcile/utils/grouping.py,sha256=kWKivD14eAkiDneH_VIl_XyUdcVVQgiaKA9sLsuD2dw,441
|
@@ -702,7 +703,7 @@ reconcile/utils/structs.py,sha256=LcbLEg8WxfRqM6nW7NhcWN0YeqF7SQzxOgntmLs1SgY,35
|
|
702
703
|
reconcile/utils/template.py,sha256=wTvRU4AnAV_o042tD4Mwls2dwWMuk7MKnde3MaCjaYg,331
|
703
704
|
reconcile/utils/terraform_client.py,sha256=mZEKpu6nbfiQd60wRkc8-5sljBTUTOgaAKnF89itMzU,32085
|
704
705
|
reconcile/utils/terrascript_aws_client.py,sha256=msKley24-PiYt3lsPUNzKh2tpMj4MpGjF7lXFr0sBM0,276187
|
705
|
-
reconcile/utils/three_way_diff_strategy.py,sha256=
|
706
|
+
reconcile/utils/three_way_diff_strategy.py,sha256=OniTnogBkdgy_7Xg51N1MgjS-Qtk8uM1ccjWaiXxiV8,4895
|
706
707
|
reconcile/utils/throughput.py,sha256=iP4UWAe2LVhDo69mPPmgo9nQ7RxHD6_GS8MZe-aSiuM,344
|
707
708
|
reconcile/utils/vault.py,sha256=AYGG5aDJ7CSVhTFdZowfEg3iSQWenoAt676aGjHQMX8,14978
|
708
709
|
reconcile/utils/vaultsecretref.py,sha256=3Ed2uBy36TzSvL0B-l4FoWQqB2SbBKDKEuUPIO608Bo,931
|
@@ -835,8 +836,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvf
|
|
835
836
|
tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jrss,4941
|
836
837
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
837
838
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
838
|
-
qontract_reconcile-0.10.
|
839
|
-
qontract_reconcile-0.10.
|
840
|
-
qontract_reconcile-0.10.
|
841
|
-
qontract_reconcile-0.10.
|
842
|
-
qontract_reconcile-0.10.
|
839
|
+
qontract_reconcile-0.10.1rc863.dist-info/METADATA,sha256=y5iyh4zPDNPYn2dGYtX6BC-ak-Fy21lC_RxGqXh0BjA,2273
|
840
|
+
qontract_reconcile-0.10.1rc863.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
841
|
+
qontract_reconcile-0.10.1rc863.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
842
|
+
qontract_reconcile-0.10.1rc863.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
843
|
+
qontract_reconcile-0.10.1rc863.dist-info/RECORD,,
|
reconcile/gitlab_permissions.py
CHANGED
@@ -8,8 +8,10 @@ from reconcile import queries
|
|
8
8
|
from reconcile.utils import batches
|
9
9
|
from reconcile.utils.defer import defer
|
10
10
|
from reconcile.utils.gitlab_api import GitLabApi
|
11
|
+
from reconcile.utils.unleash import get_feature_toggle_state
|
11
12
|
|
12
13
|
QONTRACT_INTEGRATION = "gitlab-permissions"
|
14
|
+
APP_SRE_GROUP_NAME = "app-sre"
|
13
15
|
PAGE_SIZE = 100
|
14
16
|
|
15
17
|
|
@@ -50,6 +52,19 @@ def run(dry_run, thread_pool_size=10, defer=None):
|
|
50
52
|
if defer:
|
51
53
|
defer(gl.cleanup)
|
52
54
|
repos = queries.get_repos(server=gl.server, exclude_manage_permissions=True)
|
55
|
+
share_with_group_enabled = get_feature_toggle_state(
|
56
|
+
"gitlab-permissions-share-with-group",
|
57
|
+
default=False,
|
58
|
+
)
|
59
|
+
if share_with_group_enabled:
|
60
|
+
share_project_with_group(gl, repos, dry_run)
|
61
|
+
else:
|
62
|
+
share_project_with_group_members(gl, repos, thread_pool_size, dry_run)
|
63
|
+
|
64
|
+
|
65
|
+
def share_project_with_group_members(
|
66
|
+
gl: GitLabApi, repos: list[str], thread_pool_size: int, dry_run: bool
|
67
|
+
) -> None:
|
53
68
|
app_sre = gl.get_app_sre_group_users()
|
54
69
|
results = threaded.run(
|
55
70
|
get_members_to_add, repos, thread_pool_size, gl=gl, app_sre=app_sre
|
@@ -61,6 +76,14 @@ def run(dry_run, thread_pool_size=10, defer=None):
|
|
61
76
|
gl.add_project_member(m["repo"], m["user"])
|
62
77
|
|
63
78
|
|
79
|
+
def share_project_with_group(gl: GitLabApi, repos: list[str], dry_run: bool) -> None:
|
80
|
+
group_id, shared_projects = gl.get_group_id_and_shared_projects(APP_SRE_GROUP_NAME)
|
81
|
+
shared_project_repos = {project["web_url"] for project in shared_projects}
|
82
|
+
repos_to_share = set(repos) - shared_project_repos
|
83
|
+
for repo in repos_to_share:
|
84
|
+
gl.share_project_with_group(repo_url=repo, group_id=group_id, dry_run=dry_run)
|
85
|
+
|
86
|
+
|
64
87
|
def early_exit_desired_state(*args, **kwargs) -> dict[str, Any]:
|
65
88
|
instance = queries.get_gitlab_instance()
|
66
89
|
return {
|
@@ -0,0 +1,57 @@
|
|
1
|
+
from unittest.mock import MagicMock, create_autospec
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
from gitlab.v4.objects import CurrentUser, GroupMember
|
5
|
+
from pytest_mock import MockerFixture
|
6
|
+
|
7
|
+
from reconcile import gitlab_permissions
|
8
|
+
from reconcile.utils.gitlab_api import GitLabApi
|
9
|
+
|
10
|
+
|
11
|
+
@pytest.fixture()
|
12
|
+
def mocked_queries(mocker: MockerFixture) -> MagicMock:
|
13
|
+
queries = mocker.patch("reconcile.gitlab_permissions.queries")
|
14
|
+
queries.get_gitlab_instance.return_value = {}
|
15
|
+
queries.get_app_interface_settings.return_value = {}
|
16
|
+
queries.get_repos.return_value = ["https://test-gitlab.com"]
|
17
|
+
return queries
|
18
|
+
|
19
|
+
|
20
|
+
@pytest.fixture()
|
21
|
+
def mocked_gl() -> MagicMock:
|
22
|
+
gl = create_autospec(GitLabApi)
|
23
|
+
gl.server = "test_server"
|
24
|
+
gl.user = create_autospec(CurrentUser)
|
25
|
+
gl.user.username = "test_name"
|
26
|
+
return gl
|
27
|
+
|
28
|
+
|
29
|
+
def test_run_share_with_members(
|
30
|
+
mocked_queries: MagicMock, mocker: MockerFixture, mocked_gl: MagicMock
|
31
|
+
) -> None:
|
32
|
+
mocker.patch("reconcile.gitlab_permissions.GitLabApi").return_value = mocked_gl
|
33
|
+
mocked_gl.get_app_sre_group_users.return_value = [
|
34
|
+
create_autospec(GroupMember, id=123, username="test_name2")
|
35
|
+
]
|
36
|
+
mocker.patch(
|
37
|
+
"reconcile.gitlab_permissions.get_feature_toggle_state"
|
38
|
+
).return_value = False
|
39
|
+
mocked_gl.get_project_maintainers.return_value = ["test_name"]
|
40
|
+
|
41
|
+
gitlab_permissions.run(False, thread_pool_size=1)
|
42
|
+
mocked_gl.add_project_member.assert_called_once()
|
43
|
+
|
44
|
+
|
45
|
+
def test_run_share_with_group(
|
46
|
+
mocked_queries: MagicMock, mocker: MockerFixture, mocked_gl: MagicMock
|
47
|
+
) -> None:
|
48
|
+
mocker.patch("reconcile.gitlab_permissions.GitLabApi").return_value = mocked_gl
|
49
|
+
mocker.patch(
|
50
|
+
"reconcile.gitlab_permissions.get_feature_toggle_state"
|
51
|
+
).return_value = True
|
52
|
+
mocked_gl.get_group_id_and_shared_projects.return_value = (
|
53
|
+
1234,
|
54
|
+
[{"web_url": "https://test.com"}],
|
55
|
+
)
|
56
|
+
gitlab_permissions.run(False, thread_pool_size=1)
|
57
|
+
mocked_gl.share_project_with_group.assert_called_once()
|
@@ -119,3 +119,13 @@ def test_3wpd_diff_detects_missing_annotation(deployment):
|
|
119
119
|
del c_item.body["metadata"]["annotations"]["new-annotation"]
|
120
120
|
|
121
121
|
assert three_way_diff_using_hash(c_item, d_item) is False
|
122
|
+
|
123
|
+
|
124
|
+
def test_3wpd_diff_detects_switch_integration(deployment):
|
125
|
+
d_item = OR(deployment, "same-integration", "")
|
126
|
+
deployment["metadata"]["annotations"]["qontract.integration"] = (
|
127
|
+
"different-integration"
|
128
|
+
)
|
129
|
+
c_item = OR(deployment, "same-integration", "").annotate(canonicalize=False)
|
130
|
+
|
131
|
+
assert three_way_diff_using_hash(c_item, d_item) is False
|
reconcile/utils/gitlab_api.py
CHANGED
@@ -19,6 +19,13 @@ from urllib.parse import urlparse
|
|
19
19
|
|
20
20
|
import gitlab
|
21
21
|
import urllib3
|
22
|
+
from gitlab.const import (
|
23
|
+
DEVELOPER_ACCESS,
|
24
|
+
GUEST_ACCESS,
|
25
|
+
MAINTAINER_ACCESS,
|
26
|
+
OWNER_ACCESS,
|
27
|
+
REPORTER_ACCESS,
|
28
|
+
)
|
22
29
|
from gitlab.v4.objects import (
|
23
30
|
CurrentUser,
|
24
31
|
Group,
|
@@ -250,6 +257,46 @@ class GitLabApi: # pylint: disable=too-many-public-methods
|
|
250
257
|
except gitlab.exceptions.GitlabGetError:
|
251
258
|
return None
|
252
259
|
|
260
|
+
def share_project_with_group(
|
261
|
+
self, repo_url: str, group_id: int, dry_run: bool, access: str = "maintainer"
|
262
|
+
) -> None:
|
263
|
+
project = self.get_project(repo_url)
|
264
|
+
if project is None:
|
265
|
+
return None
|
266
|
+
access_level = self.get_access_level(access)
|
267
|
+
# check if we have 'access_level' access so we can add the group with same role.
|
268
|
+
members = self.get_items(
|
269
|
+
project.members.all, query_parameters={"user_ids": self.user.id}
|
270
|
+
)
|
271
|
+
if not any(
|
272
|
+
self.user.id == member.id and member.access_level >= access_level
|
273
|
+
for member in members
|
274
|
+
):
|
275
|
+
logging.error(
|
276
|
+
"%s is not shared with %s as %s",
|
277
|
+
repo_url,
|
278
|
+
self.user.username,
|
279
|
+
access,
|
280
|
+
)
|
281
|
+
return None
|
282
|
+
logging.info(["add_group_as_maintainer", repo_url, group_id])
|
283
|
+
if not dry_run:
|
284
|
+
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
285
|
+
project.share(group_id, access_level)
|
286
|
+
|
287
|
+
def get_group_id_and_shared_projects(
|
288
|
+
self, group_name: str
|
289
|
+
) -> tuple[int, list[dict]]:
|
290
|
+
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
291
|
+
group = self.gl.groups.get(group_name)
|
292
|
+
return group.id, [
|
293
|
+
project
|
294
|
+
for project in group.shared_projects
|
295
|
+
for shared_group in project["shared_with_groups"]
|
296
|
+
if shared_group["group_id"] == group.id
|
297
|
+
and shared_group["group_access_level"] >= MAINTAINER_ACCESS
|
298
|
+
]
|
299
|
+
|
253
300
|
@staticmethod
|
254
301
|
def _is_bot_username(username: str) -> bool:
|
255
302
|
"""crudely checking for the username
|
@@ -331,30 +378,30 @@ class GitLabApi: # pylint: disable=too-many-public-methods
|
|
331
378
|
|
332
379
|
@staticmethod
|
333
380
|
def get_access_level_string(access_level):
|
334
|
-
if access_level ==
|
381
|
+
if access_level == OWNER_ACCESS:
|
335
382
|
return "owner"
|
336
|
-
if access_level ==
|
383
|
+
if access_level == MAINTAINER_ACCESS:
|
337
384
|
return "maintainer"
|
338
|
-
if access_level ==
|
385
|
+
if access_level == DEVELOPER_ACCESS:
|
339
386
|
return "developer"
|
340
|
-
if access_level ==
|
387
|
+
if access_level == REPORTER_ACCESS:
|
341
388
|
return "reporter"
|
342
|
-
if access_level ==
|
389
|
+
if access_level == GUEST_ACCESS:
|
343
390
|
return "guest"
|
344
391
|
|
345
392
|
@staticmethod
|
346
393
|
def get_access_level(access):
|
347
394
|
access = access.lower()
|
348
395
|
if access == "owner":
|
349
|
-
return
|
396
|
+
return OWNER_ACCESS
|
350
397
|
if access == "maintainer":
|
351
|
-
return
|
398
|
+
return MAINTAINER_ACCESS
|
352
399
|
if access == "developer":
|
353
|
-
return
|
400
|
+
return DEVELOPER_ACCESS
|
354
401
|
if access == "reporter":
|
355
|
-
return
|
402
|
+
return REPORTER_ACCESS
|
356
403
|
if access == "guest":
|
357
|
-
return
|
404
|
+
return GUEST_ACCESS
|
358
405
|
|
359
406
|
def get_group_id_and_projects(self, group_name: str) -> tuple[str, list[str]]:
|
360
407
|
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
@@ -131,6 +131,14 @@ def three_way_diff_using_hash(c_item: OR, d_item: OR) -> bool:
|
|
131
131
|
logging.debug("Current object QR hash is missing -> Apply")
|
132
132
|
return False
|
133
133
|
|
134
|
+
if (
|
135
|
+
c_item_integration := annotations["qontract.integration"]
|
136
|
+
) != d_item.integration:
|
137
|
+
logging.info(
|
138
|
+
f"resource switching integration from {c_item_integration} to {d_item.integration}"
|
139
|
+
)
|
140
|
+
return False
|
141
|
+
|
134
142
|
# Original object does not match Desired -> Apply
|
135
143
|
# Current object hash is not recalculated!
|
136
144
|
if c_item_sha256 != d_item.sha256sum():
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc863.dist-info}/top_level.txt
RENAMED
File without changes
|