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.
- {qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/RECORD +10 -10
- reconcile/dynatrace_token_provider/integration.py +44 -7
- reconcile/dynatrace_token_provider/metrics.py +10 -0
- reconcile/gitlab_permissions.py +130 -22
- reconcile/test/test_gitlab_permissions.py +97 -9
- reconcile/utils/gitlab_api.py +9 -40
- {qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.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.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
|
{qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
185
|
-
reconcile/dynatrace_token_provider/metrics.py,sha256=
|
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=
|
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=
|
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.
|
871
|
-
qontract_reconcile-0.10.
|
872
|
-
qontract_reconcile-0.10.
|
873
|
-
qontract_reconcile-0.10.
|
874
|
-
qontract_reconcile-0.10.
|
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"
|
reconcile/gitlab_permissions.py
CHANGED
@@ -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.
|
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
|
-
|
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
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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)
|
reconcile/utils/gitlab_api.py
CHANGED
@@ -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
|
-
|
265
|
+
project: Project,
|
266
266
|
group_id: int,
|
267
|
-
|
268
|
-
access: str = "maintainer",
|
267
|
+
access_level: int,
|
269
268
|
reshare: bool = False,
|
270
269
|
) -> None:
|
271
|
-
|
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.
|
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
|
-
|
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})
|
{qontract_reconcile-0.10.1rc1037.dist-info → qontract_reconcile-0.10.1rc1039.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|