qontract-reconcile 0.10.1rc1037__py3-none-any.whl → 0.10.1rc1039__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.1rc1037
3
+ Version: 0.10.1rc1039
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=4xJHmVX155fclrHqkR926sL1GH6RTN5XfZ8PnqNXbRA,4
35
35
  reconcile/gitlab_members.py,sha256=PrJE9OhDRdGG_gHM_77nQojLb4B18jtUu8DxgLsRS88,8417
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=wq0jbWWPN5iOWnRLuZeemCgKh4a_iGy2d4Iq3WFJykM,3636
38
+ reconcile/gitlab_permissions.py,sha256=tEjbpr-QH03vZ4L6CuVJhkgA3oaB_gyJ7d58MTR4JJQ,7389
39
39
  reconcile/gitlab_projects.py,sha256=K3tFf_aD1W4Ijp5q-9Qek3kwFGEWPcZ1kd7tzFJ4GyQ,1781
40
40
  reconcile/integrations_manager.py,sha256=gvOhVklJDeMPURxLjV30Q4hnLET3BZ-NeEEtQBoo_E0,9500
41
41
  reconcile/jenkins_base.py,sha256=0Gocu3fU2YTltaxBlbDQOUvP-7CP2OSQV1ZRwtWeVXw,875
@@ -181,8 +181,8 @@ reconcile/cna/assets/asset_factory.py,sha256=7T7X_J6xIsoGETqBRI45_EyIKEdQcnRPt_G
181
181
  reconcile/cna/assets/null.py,sha256=85mVh97atCoC0aLuX47poTZiyOthmziJeBsUw0c924w,1658
182
182
  reconcile/dynatrace_token_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
183
183
  reconcile/dynatrace_token_provider/dependencies.py,sha256=FuRUnK18EyJIIgFwQBZSskIG08mN2VQAcAcaJFTf8zc,2812
184
- reconcile/dynatrace_token_provider/integration.py,sha256=OaRX5B8KiBvXQ57U8dLchlY2pqep5OM1p8HUNJGKGkc,23196
185
- reconcile/dynatrace_token_provider/metrics.py,sha256=xiKkl8fTEBQaXJelGCPNTZhHAWdO1M3pCXNr_Tei63c,1285
184
+ reconcile/dynatrace_token_provider/integration.py,sha256=kECEWCIU_rTX7BeFm85Hjpx76GRm3Y54BFUwxQcLW7E,24682
185
+ reconcile/dynatrace_token_provider/metrics.py,sha256=oP-6NTZENFdvWiS0krnmX6tq3xyOzQ8e6vS0CZWYUuw,1496
186
186
  reconcile/dynatrace_token_provider/model.py,sha256=gkpqo5rRRueBXnIMjp4EEHqBUBuU65TRI8zpdb8GJ0A,241
187
187
  reconcile/dynatrace_token_provider/ocm.py,sha256=iHMsgbsLs-dlrB9UXmWNDF7E4UDe49JOsLa9rnowKfo,4282
188
188
  reconcile/dynatrace_token_provider/validate.py,sha256=40_9QmHoB3-KBc0k_0D4QO00PpNNPS-gU9Z6cIcWga8,1920
@@ -519,7 +519,7 @@ reconcile/test/test_github_repo_invites.py,sha256=WrPn5ROAoJYK0ihzlZcFR0V9Pu2HcM
519
519
  reconcile/test/test_gitlab_housekeeping.py,sha256=ScD9Tgf9OOgGfAFfTy6Kn2222l2wH_A3gMRKdpvoWe0,10053
520
520
  reconcile/test/test_gitlab_labeler.py,sha256=PmYXiU2g0_O5OTdMGPzdeqBAfat92U9bhjjfeyiGSmQ,4336
521
521
  reconcile/test/test_gitlab_members.py,sha256=yjfQRUFG_F0kLYegax4_ec5VIBAnCPrvAgqMcN1GXzc,9985
522
- reconcile/test/test_gitlab_permissions.py,sha256=vPFEsdjyP-lO8pc2rN6acMns3Sjz9YJs16msbBR8DZc,2736
522
+ reconcile/test/test_gitlab_permissions.py,sha256=aMf5SUeVp-aQ1bWGQPQLYa85auzRlyfZVTWqyybJ6mo,5850
523
523
  reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
524
524
  reconcile/test/test_integrations_manager.py,sha256=xpyQAVz57wAbovrcQzAeuyq8VzdItUyW2d2kp1WW_5c,38184
525
525
  reconcile/test/test_jenkins_worker_fleets.py,sha256=o1jlT7OBBSgu0M3iI4xMdz_x6SciF7yhNBpLk5gTJfg,2361
@@ -676,7 +676,7 @@ reconcile/utils/filtering.py,sha256=S4PbMHuFr3ED0P2Q_ea5CAaB7FimI62B-F5YTaKrphA,
676
676
  reconcile/utils/git.py,sha256=JkpbUO10oBTtNHZ1IhjyG6dTOUizc7I5H0vm7NvDVNw,1409
677
677
  reconcile/utils/git_secrets.py,sha256=y1rEhwA8DyDpBSAEuhMS7Y2X3mpxT2zQ4zyDFkhLe_g,1936
678
678
  reconcile/utils/github_api.py,sha256=R8OvqyPdnRqvP-Efnv9RvIcbBlb4M0KC4RlbnJMD0Tg,2426
679
- reconcile/utils/gitlab_api.py,sha256=WfhLhOp3-cOwgptiCNPhWja74Lo41ZIlJ6HFSWaIDRw,30512
679
+ reconcile/utils/gitlab_api.py,sha256=C1nsHQKKybsmFdaG9vsItBjJm69ym4VWbqbKfAEf7oY,29305
680
680
  reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
681
681
  reconcile/utils/gql.py,sha256=C0thIm_k9MBldfqwHzyqtYZk9sIvMdm9IbbnXLGwjD8,14158
682
682
  reconcile/utils/grouping.py,sha256=vr9SFHZ7bqmHYrvYcEZt-Er3-yQYfAAdq5sHLZVmXPY,456
@@ -867,8 +867,8 @@ tools/test/test_qontract_cli.py,sha256=_D61RFGAN5x44CY1tYbouhlGXXABwYfxKSWSQx3Jr
867
867
  tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
868
868
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
869
869
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
870
- qontract_reconcile-0.10.1rc1037.dist-info/METADATA,sha256=6F_72A6-bCXEcMeL8AmkvcyhzIc8L2tYbszCURoyl8w,2213
871
- qontract_reconcile-0.10.1rc1037.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
872
- qontract_reconcile-0.10.1rc1037.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
873
- qontract_reconcile-0.10.1rc1037.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
874
- qontract_reconcile-0.10.1rc1037.dist-info/RECORD,,
870
+ qontract_reconcile-0.10.1rc1039.dist-info/METADATA,sha256=Lg9_6kfW_Ccmxbo_ROXjegslf_Ji5aZZwd8RHgra-Xc,2213
871
+ qontract_reconcile-0.10.1rc1039.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
872
+ qontract_reconcile-0.10.1rc1039.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
873
+ qontract_reconcile-0.10.1rc1039.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
874
+ qontract_reconcile-0.10.1rc1039.dist-info/RECORD,,
@@ -1,14 +1,17 @@
1
1
  import base64
2
2
  import hashlib
3
3
  import logging
4
+ from collections import Counter, defaultdict
4
5
  from collections.abc import Iterable, Mapping, MutableMapping
5
6
  from datetime import timedelta
7
+ from threading import Lock
6
8
  from typing import Any
7
9
 
8
10
  from reconcile.dynatrace_token_provider.dependencies import Dependencies
9
11
  from reconcile.dynatrace_token_provider.metrics import (
10
12
  DTPClustersManagedGauge,
11
13
  DTPOrganizationErrorRate,
14
+ DTPTokensManagedGauge,
12
15
  )
13
16
  from reconcile.dynatrace_token_provider.model import DynatraceAPIToken, K8sSecret
14
17
  from reconcile.dynatrace_token_provider.ocm import (
@@ -56,6 +59,11 @@ class ReconcileErrorSummary(Exception):
56
59
  class DynatraceTokenProviderIntegration(
57
60
  QontractReconcileIntegration[DynatraceTokenProviderIntegrationParams]
58
61
  ):
62
+ def __init__(self, params: DynatraceTokenProviderIntegrationParams) -> None:
63
+ super().__init__(params=params)
64
+ self._lock = Lock()
65
+ self._managed_tokens_cnt: dict[str, Counter[str]] = defaultdict(Counter)
66
+
59
67
  @property
60
68
  def name(self) -> str:
61
69
  return QONTRACT_INTEGRATION
@@ -70,6 +78,22 @@ class DynatraceTokenProviderIntegration(
70
78
  dependencies.populate_all()
71
79
  self.reconcile(dry_run=dry_run, dependencies=dependencies)
72
80
 
81
+ def _token_cnt(self, dt_tenant_id: str, ocm_env_name: str) -> None:
82
+ with self._lock:
83
+ self._managed_tokens_cnt[ocm_env_name][dt_tenant_id] += 1
84
+
85
+ def _expose_token_metrics(self) -> None:
86
+ for ocm_env_name, counter_by_tenant_id in self._managed_tokens_cnt.items():
87
+ for dt_tenant_id, cnt in counter_by_tenant_id.items():
88
+ metrics.set_gauge(
89
+ DTPTokensManagedGauge(
90
+ integration=self.name,
91
+ ocm_env=ocm_env_name,
92
+ dt_tenant_id=dt_tenant_id,
93
+ ),
94
+ cnt,
95
+ )
96
+
73
97
  def reconcile(self, dry_run: bool, dependencies: Dependencies) -> None:
74
98
  token_specs = list(dependencies.token_spec_by_name.values())
75
99
  validate_token_specs(specs=token_specs)
@@ -86,13 +110,6 @@ class DynatraceTokenProviderIntegration(
86
110
  )
87
111
  except Exception as e:
88
112
  unhandled_exceptions.append(f"{ocm_env_name}: {e}")
89
- metrics.set_gauge(
90
- DTPClustersManagedGauge(
91
- integration=self.name,
92
- ocm_env=ocm_env_name,
93
- ),
94
- len(clusters),
95
- )
96
113
  if not clusters:
97
114
  continue
98
115
  if self.params.ocm_organization_ids:
@@ -101,8 +118,16 @@ class DynatraceTokenProviderIntegration(
101
118
  for cluster in clusters
102
119
  if cluster.organization_id in self.params.ocm_organization_ids
103
120
  ]
121
+
104
122
  existing_dtp_tokens: dict[str, dict[str, str]] = {}
105
123
 
124
+ metrics.set_gauge(
125
+ DTPClustersManagedGauge(
126
+ integration=self.name,
127
+ ocm_env=ocm_env_name,
128
+ ),
129
+ len(clusters),
130
+ )
106
131
  for cluster in clusters:
107
132
  try:
108
133
  with DTPOrganizationErrorRate(
@@ -162,11 +187,13 @@ class DynatraceTokenProviderIntegration(
162
187
  existing_dtp_tokens=existing_dtp_tokens[tenant_id],
163
188
  tenant_id=tenant_id,
164
189
  token_spec=token_spec,
190
+ ocm_env_name=ocm_env_name,
165
191
  )
166
192
  except Exception as e:
167
193
  unhandled_exceptions.append(
168
194
  f"{ocm_env_name}/{cluster.organization_id}/{cluster.external_id}: {e}"
169
195
  )
196
+ self._expose_token_metrics()
170
197
 
171
198
  if unhandled_exceptions:
172
199
  raise ReconcileErrorSummary(unhandled_exceptions)
@@ -180,6 +207,7 @@ class DynatraceTokenProviderIntegration(
180
207
  existing_dtp_tokens: MutableMapping[str, str],
181
208
  tenant_id: str,
182
209
  token_spec: DynatraceTokenProviderTokenSpecV1,
210
+ ocm_env_name: str,
183
211
  ) -> None:
184
212
  if cluster.organization_id not in token_spec.ocm_org_ids:
185
213
  logging.info(
@@ -247,6 +275,8 @@ class DynatraceTokenProviderIntegration(
247
275
  existing_dtp_tokens=existing_dtp_tokens,
248
276
  dt_client=dt_client,
249
277
  cluster_uuid=cluster.external_id,
278
+ dt_tenant_id=tenant_id,
279
+ ocm_env_name=ocm_env_name,
250
280
  )
251
281
  if has_diff:
252
282
  if not dry_run:
@@ -302,6 +332,8 @@ class DynatraceTokenProviderIntegration(
302
332
  cluster_uuid: str,
303
333
  dt_client: DynatraceClient,
304
334
  token_name_in_dt_api: str,
335
+ ocm_env_name: str,
336
+ dt_tenant_id: str,
305
337
  dry_run: bool,
306
338
  ) -> None:
307
339
  """
@@ -311,6 +343,7 @@ class DynatraceTokenProviderIntegration(
311
343
  A list query on the tokens does not return each tokens configuration.
312
344
  We encode the token configuration in the token name to save API calls.
313
345
  """
346
+ self._token_cnt(dt_tenant_id=dt_tenant_id, ocm_env_name=ocm_env_name)
314
347
  expected_name = self.dynatrace_token_name(spec=spec, cluster_uuid=cluster_uuid)
315
348
  if token_name_in_dt_api != expected_name:
316
349
  logging.info(
@@ -329,6 +362,8 @@ class DynatraceTokenProviderIntegration(
329
362
  existing_dtp_tokens: MutableMapping[str, str],
330
363
  dt_client: DynatraceClient,
331
364
  cluster_uuid: str,
365
+ ocm_env_name: str,
366
+ dt_tenant_id: str,
332
367
  ) -> tuple[bool, Iterable[K8sSecret]]:
333
368
  has_diff = False
334
369
  desired: list[K8sSecret] = []
@@ -362,6 +397,8 @@ class DynatraceTokenProviderIntegration(
362
397
  dt_client=dt_client,
363
398
  dry_run=dry_run,
364
399
  token_name_in_dt_api=existing_dtp_tokens[cur_token.id],
400
+ dt_tenant_id=dt_tenant_id,
401
+ ocm_env_name=ocm_env_name,
365
402
  )
366
403
  desired_tokens.append(cur_token)
367
404
  desired.append(
@@ -50,3 +50,13 @@ class DTPClustersManagedGauge(DTPBaseMetric, GaugeMetric):
50
50
  @classmethod
51
51
  def name(cls) -> str:
52
52
  return "dtp_clusters_managed"
53
+
54
+
55
+ class DTPTokensManagedGauge(DTPBaseMetric, GaugeMetric):
56
+ "Gauge for the number of tokens DTP manages"
57
+
58
+ dt_tenant_id: str
59
+
60
+ @classmethod
61
+ def name(cls) -> str:
62
+ return "dtp_tokens_managed"
@@ -1,21 +1,146 @@
1
1
  import itertools
2
2
  import logging
3
+ from dataclasses import dataclass
3
4
  from typing import Any
4
5
 
5
- from gitlab.const import MAINTAINER_ACCESS
6
+ from gitlab.v4.objects import (
7
+ GroupProject,
8
+ Project,
9
+ )
6
10
  from sretoolbox.utils import threaded
7
11
 
8
12
  from reconcile import queries
9
13
  from reconcile.utils import batches
10
14
  from reconcile.utils.defer import defer
15
+ from reconcile.utils.differ import diff_mappings
11
16
  from reconcile.utils.gitlab_api import GitLabApi
12
17
  from reconcile.utils.unleash import get_feature_toggle_state
13
18
 
14
19
  QONTRACT_INTEGRATION = "gitlab-permissions"
15
20
  APP_SRE_GROUP_NAME = "app-sre"
21
+ GROUP_ACCESS = "maintainer"
16
22
  PAGE_SIZE = 100
17
23
 
18
24
 
25
+ @dataclass
26
+ class GroupSpec:
27
+ group_name: str
28
+ group_access_level: int
29
+
30
+
31
+ class GroupAccessLevelError(Exception):
32
+ pass
33
+
34
+
35
+ class GroupPermissionHandler:
36
+ def __init__(
37
+ self, gl: GitLabApi, group_name: str, access: str, dry_run: bool
38
+ ) -> None:
39
+ self.gl = gl
40
+ self.dry_run = dry_run
41
+ self.access_level_string = access
42
+ self.access_level = self.gl.get_access_level(access)
43
+ self.group = self.gl.get_group(group_name)
44
+
45
+ def run(self, repos: list[str]) -> None:
46
+ # filter projects belonging to the same group and remove it from the state data
47
+ filtered_project_repos = self.filter_group_owned_projects(repos)
48
+ desired_state = {
49
+ project_repo_url: GroupSpec(self.group.name, self.access_level)
50
+ for project_repo_url in filtered_project_repos
51
+ }
52
+ # get all projects shared with group
53
+ shared_projects = self.gl.get_items(self.group.shared_projects.list)
54
+ current_state = {
55
+ project.web_url: self.extract_group_spec(project)
56
+ for project in shared_projects
57
+ }
58
+ self.reconcile(desired_state, current_state)
59
+
60
+ def filter_group_owned_projects(self, repos: list[str]) -> set[str]:
61
+ # get only the projects that are owned by group and its sub groups
62
+ query = {"with_shared": False, "include_subgroups": True}
63
+ group_owned_projects = self.gl.get_items(
64
+ self.group.projects.list, query_parameters=query
65
+ )
66
+ group_owned_repo_list = {project.web_url for project in group_owned_projects}
67
+ return set(repos) - group_owned_repo_list
68
+
69
+ def extract_group_spec(self, project: GroupProject) -> GroupSpec:
70
+ return next(
71
+ GroupSpec(
72
+ group_name=self.group.name,
73
+ group_access_level=group["group_access_level"],
74
+ )
75
+ for group in project.shared_with_groups
76
+ if group["group_id"] == self.group.id
77
+ )
78
+
79
+ def can_share_project(self, project: Project) -> bool:
80
+ # check if user have access greater or equal access to be shared with the group
81
+ user = project.members_all.get(id=self.gl.user.id)
82
+ return user.access_level >= self.access_level
83
+
84
+ def reconcile(
85
+ self,
86
+ desired_state: dict[str, GroupSpec],
87
+ current_state: dict[str, GroupSpec],
88
+ ) -> None:
89
+ # get the diff data
90
+ diff_data = diff_mappings(
91
+ current=current_state,
92
+ desired=desired_state,
93
+ equal=lambda current, desired: current.group_access_level
94
+ == desired.group_access_level,
95
+ )
96
+ errors: list[Exception] = []
97
+ for repo in diff_data.add:
98
+ project = self.gl.get_project(repo)
99
+ if not self.can_share_project(project):
100
+ errors.append(
101
+ GroupAccessLevelError(
102
+ f"{repo} is not shared with {self.gl.user.username} as {self.access_level_string}"
103
+ )
104
+ )
105
+ continue
106
+ logging.info([
107
+ "share",
108
+ repo,
109
+ self.group.name,
110
+ self.access_level_string,
111
+ ])
112
+ if not self.dry_run:
113
+ self.gl.share_project_with_group(
114
+ project=project,
115
+ group_id=self.group.id,
116
+ access_level=self.access_level,
117
+ )
118
+ for repo in diff_data.change:
119
+ project = self.gl.get_project(repo)
120
+ if not self.can_share_project(project):
121
+ errors.append(
122
+ GroupAccessLevelError(
123
+ f"{repo} is not shared with {self.gl.user.username} as {self.access_level_string}"
124
+ )
125
+ )
126
+ continue
127
+ logging.info([
128
+ "reshare",
129
+ repo,
130
+ self.group.name,
131
+ self.access_level_string,
132
+ ])
133
+ if not self.dry_run:
134
+ self.gl.share_project_with_group(
135
+ project=project,
136
+ group_id=self.group.id,
137
+ access_level=self.access_level,
138
+ reshare=True,
139
+ )
140
+ if errors:
141
+ raise ExceptionGroup("Reconcile errors occurred", errors)
142
+
143
+
19
144
  def get_members_to_add(repo, gl, app_sre):
20
145
  maintainers = get_all_app_sre_maintainers(repo, gl, app_sre)
21
146
  if maintainers is None:
@@ -56,7 +181,10 @@ def run(dry_run, thread_pool_size=10, defer=None):
56
181
  default=False,
57
182
  )
58
183
  if share_with_group_enabled:
59
- share_project_with_group(gl, repos, dry_run)
184
+ group_permission_handler = GroupPermissionHandler(
185
+ gl=gl, group_name=APP_SRE_GROUP_NAME, access=GROUP_ACCESS, dry_run=dry_run
186
+ )
187
+ group_permission_handler.run(repos=repos)
60
188
  else:
61
189
  share_project_with_group_members(gl, repos, thread_pool_size, dry_run)
62
190
 
@@ -75,26 +203,6 @@ def share_project_with_group_members(
75
203
  gl.add_project_member(m["repo"], m["user"])
76
204
 
77
205
 
78
- def share_project_with_group(gl: GitLabApi, repos: list[str], dry_run: bool) -> None:
79
- # get repos not owned by app-sre
80
- non_app_sre_project_repos = {repo for repo in repos if "/app-sre/" not in repo}
81
- group_id, shared_projects = gl.get_group_id_and_shared_projects(APP_SRE_GROUP_NAME)
82
- shared_project_repos = shared_projects.keys()
83
- repos_to_share = non_app_sre_project_repos - shared_project_repos
84
- repos_to_reshare = {
85
- repo
86
- for repo in non_app_sre_project_repos
87
- if (group_data := shared_projects.get(repo))
88
- and group_data["group_access_level"] < MAINTAINER_ACCESS
89
- }
90
- for repo in repos_to_share:
91
- gl.share_project_with_group(repo_url=repo, group_id=group_id, dry_run=dry_run)
92
- for repo in repos_to_reshare:
93
- gl.share_project_with_group(
94
- repo_url=repo, group_id=group_id, dry_run=dry_run, reshare=True
95
- )
96
-
97
-
98
206
  def early_exit_desired_state(*args, **kwargs) -> dict[str, Any]:
99
207
  instance = queries.get_gitlab_instance()
100
208
  return {
@@ -1,7 +1,17 @@
1
1
  from unittest.mock import MagicMock, create_autospec
2
2
 
3
3
  import pytest
4
- from gitlab.v4.objects import CurrentUser, GroupMember
4
+ from gitlab.v4.objects import (
5
+ CurrentUser,
6
+ Group,
7
+ GroupMember,
8
+ GroupProjectManager,
9
+ Project,
10
+ ProjectMember,
11
+ ProjectMemberAllManager,
12
+ SharedProject,
13
+ SharedProjectManager,
14
+ )
5
15
  from pytest_mock import MockerFixture
6
16
 
7
17
  from reconcile import gitlab_permissions
@@ -23,6 +33,7 @@ def mocked_gl() -> MagicMock:
23
33
  gl.server = "test_server"
24
34
  gl.user = create_autospec(CurrentUser)
25
35
  gl.user.username = "test_name"
36
+ gl.user.id = 1234
26
37
  return gl
27
38
 
28
39
 
@@ -49,13 +60,25 @@ def test_run_share_with_group(
49
60
  mocker.patch(
50
61
  "reconcile.gitlab_permissions.get_feature_toggle_state"
51
62
  ).return_value = True
52
- mocked_gl.get_group_id_and_shared_projects.return_value = (
53
- 1234,
54
- {"https://test.com": {"group_access_level": 30}},
63
+ group = create_autospec(Group, id=1234)
64
+ group.name = "app-sre"
65
+ group.projects = create_autospec(GroupProjectManager)
66
+ group.shared_projects = create_autospec(SharedProjectManager)
67
+ mocked_gl.get_items.side_effect = [
68
+ [],
69
+ [],
70
+ ]
71
+ mocked_gl.get_group.return_value = group
72
+ mocked_gl.get_access_level.return_value = 40
73
+ project = create_autospec(Project, web_url="https://test.com")
74
+ project.members_all = create_autospec(ProjectMemberAllManager)
75
+ project.members_all.get.return_value = create_autospec(
76
+ ProjectMember, id=mocked_gl.user.id, access_level=40
55
77
  )
78
+ mocked_gl.get_project.return_value = project
56
79
  gitlab_permissions.run(False, thread_pool_size=1)
57
80
  mocked_gl.share_project_with_group.assert_called_once_with(
58
- repo_url="https://test-gitlab.com", group_id=1234, dry_run=False
81
+ project, group_id=1234, access_level=40
59
82
  )
60
83
 
61
84
 
@@ -66,11 +89,76 @@ def test_run_reshare_with_group(
66
89
  mocker.patch(
67
90
  "reconcile.gitlab_permissions.get_feature_toggle_state"
68
91
  ).return_value = True
69
- mocked_gl.get_group_id_and_shared_projects.return_value = (
70
- 1234,
71
- {"https://test-gitlab.com": {"group_access_level": 30}},
92
+ group = create_autospec(Group, id=1234)
93
+ group.name = "app-sre"
94
+ group.projects = create_autospec(GroupProjectManager)
95
+ group.shared_projects = create_autospec(SharedProjectManager)
96
+ mocked_gl.get_items.side_effect = [
97
+ [],
98
+ [
99
+ create_autospec(
100
+ SharedProject,
101
+ web_url="https://test-gitlab.com",
102
+ shared_with_groups=[
103
+ {
104
+ "group_access_level": 30,
105
+ "group_name": "app-sre",
106
+ "group_id": 1234,
107
+ }
108
+ ],
109
+ )
110
+ ],
111
+ ]
112
+ mocked_gl.get_group.return_value = group
113
+ mocked_gl.get_access_level.return_value = 40
114
+ project = create_autospec(Project, web_url="https://test-gitlab.com")
115
+ project.members_all = create_autospec(ProjectMemberAllManager)
116
+ project.members_all.get.return_value = create_autospec(
117
+ ProjectMember, id=mocked_gl.user.id, access_level=40
72
118
  )
119
+ mocked_gl.get_project.return_value = project
73
120
  gitlab_permissions.run(False, thread_pool_size=1)
74
121
  mocked_gl.share_project_with_group.assert_called_once_with(
75
- repo_url="https://test-gitlab.com", group_id=1234, dry_run=False, reshare=True
122
+ project=project, group_id=1234, access_level=40, reshare=True
123
+ )
124
+
125
+
126
+ def test_run_share_with_group_failed(
127
+ mocked_queries: MagicMock, mocker: MockerFixture, mocked_gl: MagicMock
128
+ ) -> None:
129
+ mocker.patch("reconcile.gitlab_permissions.GitLabApi").return_value = mocked_gl
130
+ mocker.patch(
131
+ "reconcile.gitlab_permissions.get_feature_toggle_state"
132
+ ).return_value = True
133
+ group = create_autospec(Group, id=1234)
134
+ group.name = "app-sre"
135
+ group.projects = create_autospec(GroupProjectManager)
136
+ group.shared_projects = create_autospec(SharedProjectManager)
137
+ group.projects = create_autospec(GroupProjectManager)
138
+ group.shared_projects = create_autospec(SharedProjectManager)
139
+ mocked_gl.get_items.side_effect = [
140
+ [],
141
+ [
142
+ create_autospec(
143
+ SharedProject,
144
+ web_url="https://test-gitlab.com",
145
+ shared_with_groups=[
146
+ {
147
+ "group_access_level": 30,
148
+ "group_name": "app-sre",
149
+ "group_id": 134,
150
+ }
151
+ ],
152
+ )
153
+ ],
154
+ ]
155
+ mocked_gl.get_group.return_value = group
156
+ mocked_gl.get_access_level.return_value = 40
157
+ project = create_autospec(Project)
158
+ project.members_all = create_autospec(ProjectMemberAllManager)
159
+ project.members_all.get.return_value = create_autospec(
160
+ ProjectMember, id=mocked_gl.user.id, access_level=10
76
161
  )
162
+ mocked_gl.get_project.return_value = project
163
+ with pytest.raises(Exception):
164
+ gitlab_permissions.run(False, thread_pool_size=1)
@@ -262,51 +262,16 @@ class GitLabApi: # pylint: disable=too-many-public-methods
262
262
 
263
263
  def share_project_with_group(
264
264
  self,
265
- repo_url: str,
265
+ project: Project,
266
266
  group_id: int,
267
- dry_run: bool,
268
- access: str = "maintainer",
267
+ access_level: int,
269
268
  reshare: bool = False,
270
269
  ) -> None:
271
- project = self.get_project(repo_url)
272
- if project is None:
273
- return None
274
- access_level = self.get_access_level(access)
275
- # check if we have 'access_level' access so we can add the group with same role.
276
- members = self.get_items(
277
- project.members_all.list, query_parameters={"user_ids": self.user.id}
278
- )
279
- if not any(
280
- self.user.id == member.id and member.access_level >= access_level
281
- for member in members
282
- ):
283
- logging.error(
284
- "%s is not shared with %s as %s",
285
- repo_url,
286
- self.user.username,
287
- access,
288
- )
289
- return None
290
- logging.info(["add_group_as_maintainer", repo_url, group_id])
291
- if not dry_run:
292
- if reshare:
293
- gitlab_request.labels(integration=INTEGRATION_NAME).inc()
294
- project.unshare(group_id)
270
+ if reshare:
295
271
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
296
- project.share(group_id, access_level)
297
-
298
- def get_group_id_and_shared_projects(
299
- self, group_name: str
300
- ) -> tuple[int, dict[str, Any]]:
272
+ project.unshare(group_id)
301
273
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
302
- group = self.gl.groups.get(group_name)
303
- shared_projects = self.get_items(group.projects.list)
304
- return group.id, {
305
- project.web_url: shared_group
306
- for project in shared_projects
307
- for shared_group in project.shared_with_groups
308
- if shared_group["group_id"] == group.id
309
- }
274
+ project.share(group_id, access_level)
310
275
 
311
276
  @staticmethod
312
277
  def _is_bot_username(username: str) -> bool:
@@ -419,6 +384,10 @@ class GitLabApi: # pylint: disable=too-many-public-methods
419
384
  group = self.gl.groups.get(group_name)
420
385
  return group.id, [p.name for p in self.get_items(group.projects.list)]
421
386
 
387
+ def get_group(self, group_name: str) -> Group:
388
+ gitlab_request.labels(integration=INTEGRATION_NAME).inc()
389
+ return self.gl.groups.get(group_name)
390
+
422
391
  def create_project(self, group_id, project):
423
392
  gitlab_request.labels(integration=INTEGRATION_NAME).inc()
424
393
  self.gl.projects.create({"name": project, "namespace_id": group_id})