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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc861
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
@@ -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=K6Ta59c_Hib2DI5DnryiQbDNSOJTiY5B8cGy5a9xNkI,2199
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=0QY2hzOrTVnQxDFbdOJBOIIHEKKOA5RmGftT0QXABeY,3697
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=9C6oS98w1z-pDAAz5k9kgEvRO6r7hduHoOl7cufFr5s,27400
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=Jo0M42zvG_K6ygJOSAZZTAPxF2Fkr247O1YsmDbi0TA,4641
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.1rc861.dist-info/METADATA,sha256=It9nMQuWCqX18i1qQ8klhL2EgIc89K-bhaa5wy9NeQ8,2273
839
- qontract_reconcile-0.10.1rc861.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
840
- qontract_reconcile-0.10.1rc861.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
841
- qontract_reconcile-0.10.1rc861.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
842
- qontract_reconcile-0.10.1rc861.dist-info/RECORD,,
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,,
@@ -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
@@ -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 == gitlab.OWNER_ACCESS:
381
+ if access_level == OWNER_ACCESS:
335
382
  return "owner"
336
- if access_level == gitlab.MAINTAINER_ACCESS:
383
+ if access_level == MAINTAINER_ACCESS:
337
384
  return "maintainer"
338
- if access_level == gitlab.DEVELOPER_ACCESS:
385
+ if access_level == DEVELOPER_ACCESS:
339
386
  return "developer"
340
- if access_level == gitlab.REPORTER_ACCESS:
387
+ if access_level == REPORTER_ACCESS:
341
388
  return "reporter"
342
- if access_level == gitlab.GUEST_ACCESS:
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 gitlab.OWNER_ACCESS
396
+ return OWNER_ACCESS
350
397
  if access == "maintainer":
351
- return gitlab.MAINTAINER_ACCESS
398
+ return MAINTAINER_ACCESS
352
399
  if access == "developer":
353
- return gitlab.DEVELOPER_ACCESS
400
+ return DEVELOPER_ACCESS
354
401
  if access == "reporter":
355
- return gitlab.REPORTER_ACCESS
402
+ return REPORTER_ACCESS
356
403
  if access == "guest":
357
- return gitlab.GUEST_ACCESS
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():