qontract-reconcile 0.10.1rc861__py3-none-any.whl → 0.10.1rc862__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.1rc862.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.dist-info}/RECORD +8 -7
- reconcile/gitlab_permissions.py +23 -0
- reconcile/test/test_gitlab_permissions.py +57 -0
- reconcile/utils/gitlab_api.py +57 -10
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.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.1rc862
|
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.1rc862.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
|
@@ -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
|
@@ -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.1rc862.dist-info/METADATA,sha256=NyCtuFntbeQx8o2HfebLbLpZ_q3fqxZgXGUUgOY7h2Y,2273
|
840
|
+
qontract_reconcile-0.10.1rc862.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
841
|
+
qontract_reconcile-0.10.1rc862.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
842
|
+
qontract_reconcile-0.10.1rc862.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
843
|
+
qontract_reconcile-0.10.1rc862.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()
|
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()
|
File without changes
|
File without changes
|
{qontract_reconcile-0.10.1rc861.dist-info → qontract_reconcile-0.10.1rc862.dist-info}/top_level.txt
RENAMED
File without changes
|