qontract-reconcile 0.10.1rc1180__py3-none-any.whl → 0.10.1rc1182__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.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/RECORD +11 -9
- reconcile/gitlab_members.py +119 -122
- reconcile/test/test_gitlab_members.py +118 -211
- reconcile/utils/gitlab_api.py +23 -38
- tools/cli_commands/container_images_report.py +129 -0
- tools/qontract_cli.py +71 -1
- tools/test/test_get_container_images.py +187 -0
- {qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/top_level.txt +0 -0
{qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.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.1rc1182
|
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.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/RECORD
RENAMED
@@ -32,7 +32,7 @@ reconcile/github_validator.py,sha256=cVTVxJIGR4a1Jz8wrdXEAb_CMpXUzvykVmUURX4cook
|
|
32
32
|
reconcile/gitlab_fork_compliance.py,sha256=c7UfqSAsW04c1bWJmXXaQDwtUcG4Kb6nCJAyRU2uAuw,4449
|
33
33
|
reconcile/gitlab_housekeeping.py,sha256=D0DOqC-xuMBMct04_MI8Lq32OAi_QMvvGLOz_E-77Dw,22482
|
34
34
|
reconcile/gitlab_labeler.py,sha256=4xJHmVX155fclrHqkR926sL1GH6RTN5XfZ8PnqNXbRA,4534
|
35
|
-
reconcile/gitlab_members.py,sha256=
|
35
|
+
reconcile/gitlab_members.py,sha256=MUIgYDLeJx2-_vMypyq2Pa17cpKdXATYhtVACS2ghpQ,8297
|
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
38
|
reconcile/gitlab_permissions.py,sha256=6ZBPnQci4-1LVJBvaUS-c0QwIJjITNKVflCeH-Y5Lbk,7507
|
@@ -520,7 +520,7 @@ reconcile/test/test_github_org.py,sha256=aGWeMhkz1fjogvaAQGjemSKR201L5gEZZOe0iMQ
|
|
520
520
|
reconcile/test/test_github_repo_invites.py,sha256=WrPn5ROAoJYK0ihzlZcFR0V9Pu2HcMs13I6nazf7hq4,3307
|
521
521
|
reconcile/test/test_gitlab_housekeeping.py,sha256=ScD9Tgf9OOgGfAFfTy6Kn2222l2wH_A3gMRKdpvoWe0,10053
|
522
522
|
reconcile/test/test_gitlab_labeler.py,sha256=PmYXiU2g0_O5OTdMGPzdeqBAfat92U9bhjjfeyiGSmQ,4336
|
523
|
-
reconcile/test/test_gitlab_members.py,sha256=
|
523
|
+
reconcile/test/test_gitlab_members.py,sha256=bupl1dsLor-913LGYCuRtJMKqDsXo_d_2nbtDe6B1Us,7016
|
524
524
|
reconcile/test/test_gitlab_permissions.py,sha256=aMf5SUeVp-aQ1bWGQPQLYa85auzRlyfZVTWqyybJ6mo,5850
|
525
525
|
reconcile/test/test_instrumented_wrappers.py,sha256=CZzhnQH0c4i7-Rxjg7-0dfFMvVPegLHL46z5NHOOCwo,608
|
526
526
|
reconcile/test/test_integrations_manager.py,sha256=xpyQAVz57wAbovrcQzAeuyq8VzdItUyW2d2kp1WW_5c,38184
|
@@ -678,7 +678,7 @@ reconcile/utils/external_resources.py,sha256=y7Wz32cOAmCsUhQ6T-1N6lktnLikGkaHQ0S
|
|
678
678
|
reconcile/utils/filtering.py,sha256=S4PbMHuFr3ED0P2Q_ea5CAaB7FimI62B-F5YTaKrphA,402
|
679
679
|
reconcile/utils/git.py,sha256=wzVIYAeKlMGW538U1mkJWUI6h_mFRUY4lawh2AR8hw4,2345
|
680
680
|
reconcile/utils/github_api.py,sha256=R8OvqyPdnRqvP-Efnv9RvIcbBlb4M0KC4RlbnJMD0Tg,2426
|
681
|
-
reconcile/utils/gitlab_api.py,sha256=
|
681
|
+
reconcile/utils/gitlab_api.py,sha256=ar3D1gaPGm71ecQReMvbMbBzCa8TRs3Pn1ZZJP_lwSw,28453
|
682
682
|
reconcile/utils/gpg.py,sha256=EKG7_fdMv8BMlV5yUdPiqoTx-KrzmVSEAl2sLkaKwWI,1123
|
683
683
|
reconcile/utils/gql.py,sha256=C0thIm_k9MBldfqwHzyqtYZk9sIvMdm9IbbnXLGwjD8,14158
|
684
684
|
reconcile/utils/grouping.py,sha256=vr9SFHZ7bqmHYrvYcEZt-Er3-yQYfAAdq5sHLZVmXPY,456
|
@@ -838,10 +838,11 @@ tools/app_interface_metrics_exporter.py,sha256=zkwkxdAUAxjdc-pzx2_oJXG25fo0Fnyd5
|
|
838
838
|
tools/app_interface_reporter.py,sha256=oZPib4HPq0aZ2Zui1QGJGk6qQdfpeihujGDBnSdKyGE,17627
|
839
839
|
tools/glitchtip_access_reporter.py,sha256=oPBnk_YoDuljU3v0FaChzOwwnk4vap1xEE67QEjzdqs,2948
|
840
840
|
tools/glitchtip_access_revalidation.py,sha256=8kbBJk04mkq28kWoRDDkfCGIF3GRg3pJrFAh1sW0dbk,2821
|
841
|
-
tools/qontract_cli.py,sha256=
|
841
|
+
tools/qontract_cli.py,sha256=KfmjfaCuyd8F68oY1hHd2tEf0PY0kLd6t8sWqR81nmc,142498
|
842
842
|
tools/sd_app_sre_alert_report.py,sha256=e9vAdyenUz2f5c8-z-5WY0wv-SJ9aePKDH2r4IwB6pc,5063
|
843
843
|
tools/template_validation.py,sha256=qpKYaTgk0GOPGa2Ct5_5sKdwIHtCAKIBGzsMPuJU5fw,3371
|
844
844
|
tools/cli_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
845
|
+
tools/cli_commands/container_images_report.py,sha256=PCJIzvUqiYmTdn5xJFcxHocCYp6dprrsJ_lkYdl3ET8,4417
|
845
846
|
tools/cli_commands/erv2.py,sha256=469qdhyaf7thpPQ4hJSurvmxBqYDJsoI8H4AigQIF7U,20737
|
846
847
|
tools/cli_commands/gpg_encrypt.py,sha256=x02JOMn834z89YSNvr5B-oJky7rR1C0begCkPh45eHk,4958
|
847
848
|
tools/cli_commands/systems_and_tools.py,sha256=EMHOF1AtUDaoSk0bbjl6oUKYAz4rTZjIBaF-6E6GspM,16816
|
@@ -876,12 +877,13 @@ tools/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
876
877
|
tools/test/conftest.py,sha256=CsDbu4otrxb7X7kXKKGyV3ZEzu3pCkgjCoCGiHNx6zc,2401
|
877
878
|
tools/test/test_app_interface_metrics_exporter.py,sha256=SX7qL3D1SIRKFo95FoQztvftCWEEf-g1mfXOtgCog-g,1271
|
878
879
|
tools/test/test_erv2.py,sha256=EAS7QuJkHisRVO9bMGxm662L5B6i66wF_mT9PAjVzrU,3128
|
880
|
+
tools/test/test_get_container_images.py,sha256=L2XzfmYAd6WZ17UXNnr8Z4iwoGcCvQ0vN6gxAZ7gEws,6097
|
879
881
|
tools/test/test_qontract_cli.py,sha256=iuzKbQ6ahinvjoQmQLBrG4shey0z-1rB6qCgS8T6dgU,5789
|
880
882
|
tools/test/test_saas_promotion_state.py,sha256=dy4kkSSAQ7bC0Xp2CociETGN-2aABEfL6FU5D9Jl00Y,6056
|
881
883
|
tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
|
882
884
|
tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
|
883
|
-
qontract_reconcile-0.10.
|
884
|
-
qontract_reconcile-0.10.
|
885
|
-
qontract_reconcile-0.10.
|
886
|
-
qontract_reconcile-0.10.
|
887
|
-
qontract_reconcile-0.10.
|
885
|
+
qontract_reconcile-0.10.1rc1182.dist-info/METADATA,sha256=gdReu60z2THXeO2P__JA2nYHErSt6uI8EoVgnxsl5ro,2213
|
886
|
+
qontract_reconcile-0.10.1rc1182.dist-info/WHEEL,sha256=bFJAMchF8aTQGUgMZzHJyDDMPTO3ToJ7x23SLJa1SVo,92
|
887
|
+
qontract_reconcile-0.10.1rc1182.dist-info/entry_points.txt,sha256=GKQqCl2j2X1BJQ69een6rHcR26PmnxnONLNOQB-nRjY,491
|
888
|
+
qontract_reconcile-0.10.1rc1182.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
|
889
|
+
qontract_reconcile-0.10.1rc1182.dist-info/RECORD,,
|
reconcile/gitlab_members.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
import enum
|
2
1
|
import logging
|
3
2
|
from collections.abc import Callable
|
4
3
|
from typing import Any
|
5
4
|
|
5
|
+
from gitlab.v4.objects import (
|
6
|
+
Group,
|
7
|
+
GroupMember,
|
8
|
+
)
|
6
9
|
from pydantic import BaseModel
|
7
10
|
|
8
11
|
from reconcile import queries
|
@@ -23,6 +26,7 @@ from reconcile.gql_definitions.gitlab_members.permissions import (
|
|
23
26
|
)
|
24
27
|
from reconcile.utils import gql
|
25
28
|
from reconcile.utils.defer import defer
|
29
|
+
from reconcile.utils.differ import diff_mappings
|
26
30
|
from reconcile.utils.exceptions import AppInterfaceSettingsError
|
27
31
|
from reconcile.utils.gitlab_api import GitLabApi
|
28
32
|
from reconcile.utils.pagerduty_api import (
|
@@ -37,50 +41,51 @@ QONTRACT_INTEGRATION = "gitlab-members"
|
|
37
41
|
|
38
42
|
class GitlabUser(BaseModel):
|
39
43
|
user: str
|
40
|
-
access_level:
|
44
|
+
access_level: int
|
41
45
|
|
42
46
|
|
43
|
-
|
47
|
+
class CurrentStateSpec(BaseModel):
|
48
|
+
members: dict[str, GroupMember]
|
44
49
|
|
50
|
+
class Config:
|
51
|
+
arbitrary_types_allowed = True
|
45
52
|
|
46
|
-
class Action(enum.Enum):
|
47
|
-
add_user_to_group = enum.auto()
|
48
|
-
remove_user_from_group = enum.auto()
|
49
|
-
change_access = enum.auto()
|
50
53
|
|
54
|
+
class DesiredStateSpec(BaseModel):
|
55
|
+
members: dict[str, GitlabUser]
|
56
|
+
|
57
|
+
class Config:
|
58
|
+
arbitrary_types_allowed = True
|
51
59
|
|
52
|
-
class Diff(BaseModel):
|
53
|
-
action: Action
|
54
|
-
group: str
|
55
|
-
user: str
|
56
|
-
access_level: str
|
57
60
|
|
61
|
+
CurrentState = dict[str, CurrentStateSpec]
|
62
|
+
DesiredState = dict[str, DesiredStateSpec]
|
58
63
|
|
59
|
-
|
64
|
+
|
65
|
+
def get_current_state(
|
66
|
+
instance: GitlabInstanceV1, gl: GitLabApi, gitlab_groups_map: dict[str, Group]
|
67
|
+
) -> CurrentState:
|
60
68
|
"""Get current gitlab group members for all managed groups."""
|
61
69
|
return {
|
62
|
-
g:
|
63
|
-
|
64
|
-
|
65
|
-
|
70
|
+
g: CurrentStateSpec(
|
71
|
+
members={
|
72
|
+
u.username: u for u in gl.get_group_members(gitlab_groups_map.get(g))
|
73
|
+
},
|
74
|
+
)
|
66
75
|
for g in instance.managed_groups
|
67
76
|
}
|
68
77
|
|
69
78
|
|
70
79
|
def add_or_update_user(
|
71
|
-
|
80
|
+
desired_state_spec: DesiredStateSpec, gitlab_user: GitlabUser
|
72
81
|
) -> None:
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
if not existing_users:
|
77
|
-
group_members[group_name].append(gitlab_user)
|
82
|
+
existing_user = desired_state_spec.members.get(gitlab_user.user)
|
83
|
+
if not existing_user:
|
84
|
+
desired_state_spec.members[gitlab_user.user] = gitlab_user
|
78
85
|
else:
|
79
|
-
existing_user =
|
80
|
-
|
81
|
-
|
82
|
-
) < GitLabApi.get_access_level(gitlab_user.access_level):
|
83
|
-
existing_user.access_level = gitlab_user.access_level
|
86
|
+
existing_user.access_level = max(
|
87
|
+
existing_user.access_level, gitlab_user.access_level
|
88
|
+
)
|
84
89
|
|
85
90
|
|
86
91
|
def get_desired_state(
|
@@ -88,95 +93,41 @@ def get_desired_state(
|
|
88
93
|
pagerduty_map: PagerDutyMap,
|
89
94
|
permissions: list[PermissionGitlabGroupMembershipV1],
|
90
95
|
all_users: list[User],
|
91
|
-
) ->
|
96
|
+
) -> DesiredState:
|
92
97
|
"""Fetch all desired gitlab users from app-interface."""
|
93
|
-
desired_group_members:
|
94
|
-
|
95
|
-
for
|
96
|
-
|
97
|
-
for r in p.roles or []:
|
98
|
-
for u in (r.users or []) + (r.bots or []):
|
99
|
-
gu = GitlabUser(user=u.org_username, access_level=p.access)
|
100
|
-
add_or_update_user(desired_group_members, g, gu)
|
101
|
-
if p.pagerduty:
|
102
|
-
usernames_from_pagerduty = get_usernames_from_pagerduty(
|
103
|
-
p.pagerduty,
|
104
|
-
all_users,
|
105
|
-
g,
|
106
|
-
pagerduty_map,
|
107
|
-
get_username_method=lambda u: u.org_username,
|
108
|
-
)
|
109
|
-
for u in usernames_from_pagerduty:
|
110
|
-
gu = GitlabUser(user=u, access_level=p.access)
|
111
|
-
add_or_update_user(desired_group_members, g, gu)
|
112
|
-
|
98
|
+
desired_group_members: DesiredState = {
|
99
|
+
g: build_desired_state_spec(g, permissions, pagerduty_map, all_users)
|
100
|
+
for g in instance.managed_groups
|
101
|
+
}
|
113
102
|
return desired_group_members
|
114
103
|
|
115
104
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
)
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
found = True
|
138
|
-
break
|
139
|
-
if not found:
|
140
|
-
result.append(
|
141
|
-
Diff(
|
142
|
-
action=action,
|
143
|
-
group=f_group,
|
144
|
-
user=f_user.user,
|
145
|
-
access_level=f_user.access_level,
|
146
|
-
)
|
105
|
+
def build_desired_state_spec(
|
106
|
+
group_name: str,
|
107
|
+
permissions: list[PermissionGitlabGroupMembershipV1],
|
108
|
+
pagerduty_map: PagerDutyMap,
|
109
|
+
all_users: list[User],
|
110
|
+
) -> DesiredStateSpec:
|
111
|
+
desired_state_spec = DesiredStateSpec(members={})
|
112
|
+
for p in permissions:
|
113
|
+
if p.group == group_name:
|
114
|
+
p_access_level = GitLabApi.get_access_level(p.access)
|
115
|
+
for r in p.roles or []:
|
116
|
+
for u in (r.users or []) + (r.bots or []):
|
117
|
+
gu = GitlabUser(user=u.org_username, access_level=p_access_level)
|
118
|
+
add_or_update_user(desired_state_spec, gu)
|
119
|
+
if p.pagerduty:
|
120
|
+
usernames_from_pagerduty = get_usernames_from_pagerduty(
|
121
|
+
p.pagerduty,
|
122
|
+
all_users,
|
123
|
+
group_name,
|
124
|
+
pagerduty_map,
|
125
|
+
get_username_method=lambda u: u.org_username,
|
147
126
|
)
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
"""Return diff objects for item where access level is different."""
|
153
|
-
result = []
|
154
|
-
for d_group, d_users in desired_state.items():
|
155
|
-
c_group = current_state[d_group]
|
156
|
-
for d_user in d_users:
|
157
|
-
for c_user in c_group:
|
158
|
-
if d_user.user == c_user.user:
|
159
|
-
if d_user.access_level != c_user.access_level:
|
160
|
-
result.append(
|
161
|
-
Diff(
|
162
|
-
action=Action.change_access,
|
163
|
-
group=d_group,
|
164
|
-
user=c_user.user,
|
165
|
-
access_level=d_user.access_level,
|
166
|
-
)
|
167
|
-
)
|
168
|
-
break
|
169
|
-
return result
|
170
|
-
|
171
|
-
|
172
|
-
def act(diff: Diff, gl: GitLabApi) -> None:
|
173
|
-
"""Apply a diff object."""
|
174
|
-
if diff.action == Action.remove_user_from_group:
|
175
|
-
gl.remove_group_member(diff.group, diff.user)
|
176
|
-
if diff.action == Action.add_user_to_group:
|
177
|
-
gl.add_group_member(diff.group, diff.user, diff.access_level)
|
178
|
-
if diff.action == Action.change_access:
|
179
|
-
gl.change_access(diff.group, diff.user, diff.access_level)
|
127
|
+
for u in usernames_from_pagerduty:
|
128
|
+
gu = GitlabUser(user=u, access_level=p_access_level)
|
129
|
+
add_or_update_user(desired_state_spec, gu)
|
130
|
+
return desired_state_spec
|
180
131
|
|
181
132
|
|
182
133
|
def get_permissions(query_func: Callable) -> list[PermissionGitlabGroupMembershipV1]:
|
@@ -197,6 +148,11 @@ def get_gitlab_instance(query_func: Callable) -> GitlabInstanceV1:
|
|
197
148
|
raise AppInterfaceSettingsError("No gitlab instance found!")
|
198
149
|
|
199
150
|
|
151
|
+
def get_managed_groups_map(group_names: list[str], gl: GitLabApi) -> dict[str, Group]:
|
152
|
+
gitlab_groups = {group_name: gl.get_group(group_name) for group_name in group_names}
|
153
|
+
return gitlab_groups
|
154
|
+
|
155
|
+
|
200
156
|
@defer
|
201
157
|
def run(
|
202
158
|
dry_run: bool,
|
@@ -228,17 +184,58 @@ def run(
|
|
228
184
|
pagerduty_map = get_pagerduty_map(
|
229
185
|
secret_reader, pagerduty_instances=pagerduty_instances
|
230
186
|
)
|
231
|
-
|
232
|
-
|
233
|
-
current_state = get_current_state(instance, gl)
|
187
|
+
managed_groups_map = get_managed_groups_map(instance.managed_groups, gl)
|
188
|
+
current_state = get_current_state(instance, gl, managed_groups_map)
|
234
189
|
desired_state = get_desired_state(instance, pagerduty_map, permissions, all_users)
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
190
|
+
for group_name in instance.managed_groups:
|
191
|
+
reconcile_gitlab_members(
|
192
|
+
current_state_spec=current_state.get(group_name),
|
193
|
+
desired_state_spec=desired_state.get(group_name),
|
194
|
+
group=managed_groups_map.get(group_name),
|
195
|
+
gl=gl,
|
196
|
+
dry_run=dry_run,
|
197
|
+
)
|
198
|
+
|
199
|
+
|
200
|
+
def reconcile_gitlab_members(
|
201
|
+
current_state_spec: CurrentStateSpec | None,
|
202
|
+
desired_state_spec: DesiredStateSpec | None,
|
203
|
+
group: Group | None,
|
204
|
+
gl: GitLabApi,
|
205
|
+
dry_run: bool,
|
206
|
+
) -> None:
|
207
|
+
if current_state_spec and desired_state_spec and group:
|
208
|
+
diff_data = diff_mappings(
|
209
|
+
current=current_state_spec.members,
|
210
|
+
desired=desired_state_spec.members,
|
211
|
+
equal=lambda current, desired: current.access_level == desired.access_level,
|
212
|
+
)
|
213
|
+
for key, gitlab_user in diff_data.add.items():
|
214
|
+
logging.info([
|
215
|
+
key,
|
216
|
+
"add_user_to_group",
|
217
|
+
group.name,
|
218
|
+
gl.get_access_level_string(gitlab_user.access_level),
|
219
|
+
])
|
220
|
+
if not dry_run:
|
221
|
+
gl.add_group_member(group, gitlab_user)
|
222
|
+
for key, group_member in diff_data.delete.items():
|
223
|
+
logging.info([
|
224
|
+
key,
|
225
|
+
"remove_user_from_group",
|
226
|
+
group.name,
|
227
|
+
])
|
228
|
+
if not dry_run:
|
229
|
+
gl.remove_group_member(group, group_member.id)
|
230
|
+
for key, diff_pair in diff_data.change.items():
|
231
|
+
logging.info([
|
232
|
+
key,
|
233
|
+
"change_access",
|
234
|
+
group.name,
|
235
|
+
gl.get_access_level_string(diff_pair.desired.access_level),
|
236
|
+
])
|
237
|
+
if not dry_run:
|
238
|
+
gl.change_access(diff_pair.current, diff_pair.desired.access_level)
|
242
239
|
|
243
240
|
|
244
241
|
def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
@@ -1,15 +1,20 @@
|
|
1
|
-
import copy
|
2
1
|
from typing import Any
|
2
|
+
from unittest.mock import create_autospec
|
3
3
|
|
4
4
|
import pytest
|
5
|
+
from gitlab.v4.objects import (
|
6
|
+
Group,
|
7
|
+
GroupMember,
|
8
|
+
)
|
5
9
|
from pytest_mock import MockerFixture
|
6
10
|
|
7
11
|
from reconcile import gitlab_members
|
8
12
|
from reconcile.gitlab_members import (
|
9
|
-
|
10
|
-
|
13
|
+
CurrentState,
|
14
|
+
CurrentStateSpec,
|
15
|
+
DesiredState,
|
16
|
+
DesiredStateSpec,
|
11
17
|
GitlabUser,
|
12
|
-
State,
|
13
18
|
add_or_update_user,
|
14
19
|
get_permissions,
|
15
20
|
)
|
@@ -37,16 +42,41 @@ def instance(vault_secret: VaultSecret) -> GitlabInstanceV1:
|
|
37
42
|
|
38
43
|
|
39
44
|
@pytest.fixture()
|
40
|
-
def
|
45
|
+
def gitlab_groups_map() -> dict[str, Group]:
|
46
|
+
group1 = create_autospec(Group, name="group1", id="123")
|
47
|
+
group1.name = "group1"
|
48
|
+
group2 = create_autospec(Group, name="group2", id="124")
|
49
|
+
group2.name = "group2"
|
41
50
|
return {
|
42
|
-
"group1":
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
51
|
+
"group1": group1,
|
52
|
+
"group2": group2,
|
53
|
+
}
|
54
|
+
|
55
|
+
|
56
|
+
@pytest.fixture()
|
57
|
+
def all_users() -> list[GroupMember]:
|
58
|
+
user1 = create_autospec(GroupMember, username="user1", id="123", access_level=30)
|
59
|
+
user2 = create_autospec(GroupMember, username="user2", id="124", access_level=40)
|
60
|
+
user3 = create_autospec(GroupMember, username="user3", id="125", access_level=30)
|
61
|
+
user4 = create_autospec(GroupMember, username="user4", id="126", access_level=40)
|
62
|
+
return [user1, user2, user3, user4]
|
63
|
+
|
64
|
+
|
65
|
+
@pytest.fixture()
|
66
|
+
def current_state(all_users: list[GroupMember]) -> CurrentState:
|
67
|
+
return {
|
68
|
+
"group1": CurrentStateSpec(
|
69
|
+
members={
|
70
|
+
"user1": all_users[0],
|
71
|
+
"user2": all_users[1],
|
72
|
+
},
|
73
|
+
),
|
74
|
+
"group2": CurrentStateSpec(
|
75
|
+
members={
|
76
|
+
"user3": all_users[2],
|
77
|
+
"user4": all_users[3],
|
78
|
+
},
|
79
|
+
),
|
50
80
|
}
|
51
81
|
|
52
82
|
|
@@ -72,20 +102,27 @@ def user() -> User:
|
|
72
102
|
|
73
103
|
|
74
104
|
def test_gitlab_members_get_current_state(
|
75
|
-
mocker: MockerFixture,
|
105
|
+
mocker: MockerFixture,
|
106
|
+
instance: GitlabInstanceV1,
|
107
|
+
current_state: CurrentState,
|
108
|
+
gitlab_groups_map: dict[str, Group],
|
109
|
+
all_users: list[GroupMember],
|
76
110
|
) -> None:
|
77
111
|
gl_mock = mocker.create_autospec(GitLabApi)
|
78
112
|
gl_mock.get_group_members.side_effect = [
|
79
113
|
[
|
80
|
-
|
81
|
-
|
114
|
+
all_users[0],
|
115
|
+
all_users[1],
|
82
116
|
],
|
83
117
|
[
|
84
|
-
|
85
|
-
|
118
|
+
all_users[2],
|
119
|
+
all_users[3],
|
86
120
|
],
|
87
121
|
]
|
88
|
-
assert
|
122
|
+
assert (
|
123
|
+
gitlab_members.get_current_state(instance, gl_mock, gitlab_groups_map)
|
124
|
+
== current_state
|
125
|
+
)
|
89
126
|
|
90
127
|
|
91
128
|
def test_gitlab_members_get_desired_state(
|
@@ -103,210 +140,80 @@ def test_gitlab_members_get_desired_state(
|
|
103
140
|
assert gitlab_members.get_desired_state(
|
104
141
|
instance, mock_pagerduty_map, permissions, [user]
|
105
142
|
) == {
|
106
|
-
"group1":
|
107
|
-
|
108
|
-
GitlabUser(user="devtools-bot", access_level="owner"),
|
109
|
-
GitlabUser(user="user1", access_level="owner"),
|
110
|
-
GitlabUser(user="user2", access_level="owner"),
|
111
|
-
GitlabUser(user="user3", access_level="owner"),
|
112
|
-
GitlabUser(user="user4", access_level="owner"),
|
113
|
-
GitlabUser(user="another-bot", access_level="owner"),
|
114
|
-
],
|
115
|
-
}
|
116
|
-
|
117
|
-
|
118
|
-
def test_gitlab_members_calculate_diff_no_changes(state: State) -> None:
|
119
|
-
# pylint: disable-next=use-implicit-booleaness-not-comparison # for better readability
|
120
|
-
assert gitlab_members.calculate_diff(state, state) == []
|
121
|
-
|
122
|
-
|
123
|
-
def test_gitlab_members_subtract_states_no_changes_add(state: State) -> None:
|
124
|
-
# pylint: disable-next=use-implicit-booleaness-not-comparison # for better readability
|
125
|
-
assert gitlab_members.subtract_states(state, state, Action.add_user_to_group) == []
|
126
|
-
|
127
|
-
|
128
|
-
def test_gitlab_members_subtract_states_no_changes_remove(state: State) -> None:
|
129
|
-
# pylint: disable=use-implicit-booleaness-not-comparison # for better readability
|
130
|
-
assert (
|
131
|
-
gitlab_members.subtract_states(state, state, Action.remove_user_from_group)
|
132
|
-
== []
|
133
|
-
)
|
134
|
-
|
135
|
-
|
136
|
-
def test_gitlab_members_subtract_states_add(state: State) -> None:
|
137
|
-
current_state = copy.deepcopy(state)
|
138
|
-
# enforce add users to groups
|
139
|
-
current_state["group2"] = [GitlabUser(access_level="maintainer", user="otherone")]
|
140
|
-
del current_state["group1"][1]
|
141
|
-
|
142
|
-
desired_state = state
|
143
|
-
assert gitlab_members.subtract_states(
|
144
|
-
desired_state, current_state, Action.add_user_to_group
|
145
|
-
) == [
|
146
|
-
Diff(
|
147
|
-
action=Action.add_user_to_group,
|
148
|
-
group="group1",
|
149
|
-
user="user2",
|
150
|
-
access_level="maintainer",
|
151
|
-
),
|
152
|
-
Diff(
|
153
|
-
action=Action.add_user_to_group,
|
154
|
-
group="group2",
|
155
|
-
user="user3",
|
156
|
-
access_level="developer",
|
157
|
-
),
|
158
|
-
Diff(
|
159
|
-
action=Action.add_user_to_group,
|
160
|
-
group="group2",
|
161
|
-
user="user4",
|
162
|
-
access_level="maintainer",
|
163
|
-
),
|
164
|
-
]
|
165
|
-
|
166
|
-
|
167
|
-
def test_gitlab_members_subtract_states_remove(state: State) -> None:
|
168
|
-
current_state = copy.deepcopy(state)
|
169
|
-
# enforce remove user from group
|
170
|
-
current_state["group2"] = [GitlabUser(access_level="maintainer", user="otherone")]
|
171
|
-
|
172
|
-
desired_state = state
|
173
|
-
assert gitlab_members.subtract_states(
|
174
|
-
current_state, desired_state, Action.remove_user_from_group
|
175
|
-
) == [
|
176
|
-
Diff(
|
177
|
-
action=Action.remove_user_from_group,
|
178
|
-
group="group2",
|
179
|
-
user="otherone",
|
180
|
-
access_level="maintainer",
|
181
|
-
)
|
182
|
-
]
|
183
|
-
|
184
|
-
|
185
|
-
def test_gitlab_members_check_access_no_changes(state: State) -> None:
|
186
|
-
# pylint: disable-next=use-implicit-booleaness-not-comparison # for better readability
|
187
|
-
assert gitlab_members.check_access(state, state) == []
|
188
|
-
|
189
|
-
|
190
|
-
def test_gitlab_members_check_access(state: State) -> None:
|
191
|
-
current_state = copy.deepcopy(state)
|
192
|
-
# enforce access change
|
193
|
-
current_state["group1"][0].access_level = "owner"
|
194
|
-
desired_state = state
|
195
|
-
assert gitlab_members.check_access(current_state, desired_state) == [
|
196
|
-
Diff(
|
197
|
-
action=Action.change_access,
|
198
|
-
group="group1",
|
199
|
-
user="user1",
|
200
|
-
access_level="developer",
|
201
|
-
),
|
202
|
-
]
|
203
|
-
|
204
|
-
|
205
|
-
def test_gitlab_members_calculate_diff_changes(state: State) -> None:
|
206
|
-
current_state = copy.deepcopy(state)
|
207
|
-
# enforce remove user from group
|
208
|
-
current_state["group2"] = [GitlabUser(access_level="maintainer", user="otherone")]
|
209
|
-
# enforce add user to group
|
210
|
-
del current_state["group1"][1]
|
211
|
-
# enforce access change
|
212
|
-
current_state["group1"][0].access_level = "owner"
|
213
|
-
desired_state = state
|
214
|
-
assert gitlab_members.calculate_diff(current_state, desired_state) == [
|
215
|
-
Diff(
|
216
|
-
action=Action.add_user_to_group,
|
217
|
-
group="group1",
|
218
|
-
user="user2",
|
219
|
-
access_level="maintainer",
|
220
|
-
),
|
221
|
-
Diff(
|
222
|
-
action=Action.add_user_to_group,
|
223
|
-
group="group2",
|
224
|
-
user="user3",
|
225
|
-
access_level="developer",
|
226
|
-
),
|
227
|
-
Diff(
|
228
|
-
action=Action.add_user_to_group,
|
229
|
-
group="group2",
|
230
|
-
user="user4",
|
231
|
-
access_level="maintainer",
|
232
|
-
),
|
233
|
-
Diff(
|
234
|
-
action=Action.remove_user_from_group,
|
235
|
-
group="group2",
|
236
|
-
user="otherone",
|
237
|
-
access_level="maintainer",
|
143
|
+
"group1": DesiredStateSpec(
|
144
|
+
members={"devtools-bot": GitlabUser(user="devtools-bot", access_level=50)},
|
238
145
|
),
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
146
|
+
"group2": DesiredStateSpec(
|
147
|
+
members={
|
148
|
+
"devtools-bot": GitlabUser(user="devtools-bot", access_level=50),
|
149
|
+
"user1": GitlabUser(user="user1", access_level=50),
|
150
|
+
"user2": GitlabUser(user="user2", access_level=50),
|
151
|
+
"user3": GitlabUser(user="user3", access_level=50),
|
152
|
+
"user4": GitlabUser(user="user4", access_level=50),
|
153
|
+
"another-bot": GitlabUser(user="another-bot", access_level=50),
|
154
|
+
},
|
244
155
|
),
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
def test_gitlab_members_act_add(mocker: MockerFixture) -> None:
|
249
|
-
gl_mock = mocker.create_autospec(GitLabApi)
|
250
|
-
diff = Diff(
|
251
|
-
action=Action.add_user_to_group,
|
252
|
-
group="group2",
|
253
|
-
user="user4",
|
254
|
-
access_level="maintainer",
|
255
|
-
)
|
256
|
-
gitlab_members.act(diff, gl_mock)
|
257
|
-
gl_mock.add_group_member.assert_called_once()
|
258
|
-
gl_mock.remove_group_member.assert_not_called()
|
259
|
-
gl_mock.change_access.assert_not_called()
|
260
|
-
|
261
|
-
|
262
|
-
def test_gitlab_members_act_remove(mocker: MockerFixture) -> None:
|
263
|
-
gl_mock = mocker.create_autospec(GitLabApi)
|
264
|
-
diff = Diff(
|
265
|
-
action=Action.remove_user_from_group,
|
266
|
-
group="group2",
|
267
|
-
user="otherone",
|
268
|
-
access_level="maintainer",
|
269
|
-
)
|
270
|
-
gitlab_members.act(diff, gl_mock)
|
271
|
-
gl_mock.add_group_member.assert_not_called()
|
272
|
-
gl_mock.remove_group_member.assert_called_once()
|
273
|
-
gl_mock.change_access.assert_not_called()
|
156
|
+
}
|
274
157
|
|
275
158
|
|
276
|
-
def
|
159
|
+
def test_gitlab_members_reconcile_gitlab_members(
|
160
|
+
gitlab_groups_map: dict[str, Group],
|
161
|
+
mocker: MockerFixture,
|
162
|
+
all_users: list[GroupMember],
|
163
|
+
) -> None:
|
277
164
|
gl_mock = mocker.create_autospec(GitLabApi)
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
165
|
+
current_state: CurrentState = {
|
166
|
+
"group1": CurrentStateSpec(
|
167
|
+
members={
|
168
|
+
"user1": all_users[0],
|
169
|
+
"user3": all_users[2],
|
170
|
+
"user4": all_users[3],
|
171
|
+
},
|
172
|
+
)
|
173
|
+
}
|
174
|
+
new_user = GitlabUser(user="new_user", access_level=40)
|
175
|
+
desired_state: DesiredState = {
|
176
|
+
"group1": DesiredStateSpec(
|
177
|
+
members={
|
178
|
+
"user1": GitlabUser(user="user1", access_level=30),
|
179
|
+
"new_user": new_user,
|
180
|
+
"user3": GitlabUser(user="user3", access_level=50),
|
181
|
+
}
|
182
|
+
)
|
183
|
+
}
|
184
|
+
group = gitlab_groups_map.get("group1")
|
185
|
+
gitlab_members.reconcile_gitlab_members(
|
186
|
+
current_state_spec=current_state.get("group1"),
|
187
|
+
desired_state_spec=desired_state.get("group1"),
|
188
|
+
group=group,
|
189
|
+
dry_run=False,
|
190
|
+
gl=gl_mock,
|
283
191
|
)
|
284
|
-
|
285
|
-
gl_mock.
|
286
|
-
gl_mock.remove_group_member.
|
287
|
-
gl_mock.change_access.assert_called_once()
|
192
|
+
gl_mock.add_group_member.assert_called_once_with(group, new_user)
|
193
|
+
gl_mock.change_access.assert_called_once_with(all_users[2], 50)
|
194
|
+
gl_mock.remove_group_member.assert_called_once_with(group, all_users[3].id)
|
288
195
|
|
289
196
|
|
290
197
|
def test_add_or_update_user_add():
|
291
|
-
|
292
|
-
gu = GitlabUser(user="u", access_level="
|
293
|
-
add_or_update_user(
|
294
|
-
assert
|
198
|
+
desired_state_spec: DesiredStateSpec = DesiredStateSpec(members={})
|
199
|
+
gu = GitlabUser(user="u", access_level=50, id="1234")
|
200
|
+
add_or_update_user(desired_state_spec, gu)
|
201
|
+
assert desired_state_spec == DesiredStateSpec(members={"u": gu})
|
295
202
|
|
296
203
|
|
297
204
|
def test_add_or_update_user_update_higher():
|
298
|
-
|
299
|
-
gu1 = GitlabUser(user="u", access_level=
|
300
|
-
gu2 = GitlabUser(user="u", access_level=
|
301
|
-
add_or_update_user(
|
302
|
-
add_or_update_user(
|
303
|
-
assert
|
205
|
+
desired_state_spec: DesiredStateSpec = DesiredStateSpec(members={})
|
206
|
+
gu1 = GitlabUser(user="u", access_level=40)
|
207
|
+
gu2 = GitlabUser(user="u", access_level=50)
|
208
|
+
add_or_update_user(desired_state_spec, gu1)
|
209
|
+
add_or_update_user(desired_state_spec, gu2)
|
210
|
+
assert desired_state_spec == DesiredStateSpec(members={"u": gu2})
|
304
211
|
|
305
212
|
|
306
213
|
def test_add_or_update_user_update_lower():
|
307
|
-
|
308
|
-
gu1 = GitlabUser(user="u", access_level=
|
309
|
-
gu2 = GitlabUser(user="u", access_level=
|
310
|
-
add_or_update_user(
|
311
|
-
add_or_update_user(
|
312
|
-
assert
|
214
|
+
desired_state_spec: DesiredStateSpec = DesiredStateSpec(members={})
|
215
|
+
gu1 = GitlabUser(user="u", access_level=50)
|
216
|
+
gu2 = GitlabUser(user="u", access_level=40)
|
217
|
+
add_or_update_user(desired_state_spec, gu1)
|
218
|
+
add_or_update_user(desired_state_spec, gu2)
|
219
|
+
assert desired_state_spec == DesiredStateSpec(members={"u": gu1})
|
reconcile/utils/gitlab_api.py
CHANGED
@@ -29,6 +29,7 @@ from gitlab.const import (
|
|
29
29
|
from gitlab.v4.objects import (
|
30
30
|
CurrentUser,
|
31
31
|
Group,
|
32
|
+
GroupMember,
|
32
33
|
PersonalAccessToken,
|
33
34
|
Project,
|
34
35
|
ProjectIssue,
|
@@ -84,6 +85,7 @@ GROUP_BOT_NAME_REGEX = re.compile(r"group_.+_bot_.+")
|
|
84
85
|
|
85
86
|
|
86
87
|
class GLGroupMember(TypedDict):
|
88
|
+
id: str
|
87
89
|
user: str
|
88
90
|
access_level: str
|
89
91
|
|
@@ -288,17 +290,13 @@ class GitLabApi: # pylint: disable=too-many-public-methods
|
|
288
290
|
"""
|
289
291
|
return GROUP_BOT_NAME_REGEX.match(username) is not None
|
290
292
|
|
291
|
-
def get_group_members(self,
|
292
|
-
group = self.get_group_if_exists(group_name)
|
293
|
+
def get_group_members(self, group: Group | None) -> list[GroupMember]:
|
293
294
|
if group is None:
|
294
|
-
logging.error(
|
295
|
+
logging.error("no group provided")
|
295
296
|
return []
|
296
297
|
else:
|
297
298
|
return [
|
298
|
-
|
299
|
-
"user": m.username,
|
300
|
-
"access_level": self.get_access_level_string(m.access_level),
|
301
|
-
}
|
299
|
+
m
|
302
300
|
for m in self.get_items(group.members.list)
|
303
301
|
if not self._is_bot_username(m.username)
|
304
302
|
]
|
@@ -315,40 +313,27 @@ class GitLabApi: # pylint: disable=too-many-public-methods
|
|
315
313
|
member.access_level = access_level
|
316
314
|
member.save()
|
317
315
|
|
318
|
-
def add_group_member(self,
|
319
|
-
|
320
|
-
if not
|
321
|
-
logging.error(group_name + " group not found")
|
322
|
-
else:
|
323
|
-
user = self.get_user(username)
|
324
|
-
access_level = self.get_access_level(access)
|
325
|
-
if user is not None:
|
326
|
-
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
327
|
-
try:
|
328
|
-
group.members.create({
|
329
|
-
"user_id": user.id,
|
330
|
-
"access_level": access_level,
|
331
|
-
})
|
332
|
-
except gitlab.exceptions.GitlabCreateError:
|
333
|
-
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
334
|
-
member = group.members.get(user.id)
|
335
|
-
member.access_level = access_level
|
336
|
-
|
337
|
-
def remove_group_member(self, group_name, username):
|
338
|
-
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
339
|
-
group = self.gl.groups.get(group_name)
|
340
|
-
user = self.get_user(username)
|
341
|
-
if user is not None:
|
316
|
+
def add_group_member(self, group, user):
|
317
|
+
gitlab_user = self.get_user(user.user)
|
318
|
+
if gitlab_user is not None:
|
342
319
|
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
343
|
-
|
320
|
+
try:
|
321
|
+
group.members.create({
|
322
|
+
"user_id": gitlab_user.id,
|
323
|
+
"access_level": user.access_level,
|
324
|
+
})
|
325
|
+
except gitlab.exceptions.GitlabCreateError:
|
326
|
+
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
327
|
+
member = group.members.get(user.user)
|
328
|
+
member.access_level = user.access_level
|
329
|
+
member.save()
|
344
330
|
|
345
|
-
def
|
346
|
-
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
347
|
-
group = self.gl.groups.get(group)
|
348
|
-
user = self.get_user(username)
|
331
|
+
def remove_group_member(self, group, user_id):
|
349
332
|
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
350
|
-
|
351
|
-
|
333
|
+
group.members.delete(user_id)
|
334
|
+
|
335
|
+
def change_access(self, member, access_level):
|
336
|
+
member.access_level = access_level
|
352
337
|
gitlab_request.labels(integration=INTEGRATION_NAME).inc()
|
353
338
|
member.save()
|
354
339
|
|
@@ -0,0 +1,129 @@
|
|
1
|
+
import re
|
2
|
+
from collections import defaultdict
|
3
|
+
from collections.abc import Sequence
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
from sretoolbox.utils import threaded
|
8
|
+
|
9
|
+
from reconcile.gql_definitions.common.namespaces_minimal import NamespaceV1
|
10
|
+
from reconcile.typed_queries.app_interface_vault_settings import (
|
11
|
+
get_app_interface_vault_settings,
|
12
|
+
)
|
13
|
+
from reconcile.typed_queries.namespaces_minimal import get_namespaces_minimal
|
14
|
+
from reconcile.utils.oc_filters import filter_namespaces_by_cluster_and_namespace
|
15
|
+
from reconcile.utils.oc_map import OCMap, init_oc_map_from_namespaces
|
16
|
+
from reconcile.utils.secret_reader import create_secret_reader
|
17
|
+
|
18
|
+
IMAGE_NAME_REGEX = re.compile(r"^(?P<name>[a-zA-Z0-9][a-zA-Z0-9/_.-]+)(?:@sha256)?:.+$")
|
19
|
+
|
20
|
+
|
21
|
+
class NamespaceImages(BaseModel):
|
22
|
+
namespace_name: str
|
23
|
+
image_names: list[str]
|
24
|
+
|
25
|
+
|
26
|
+
def get_all_pods_images(
|
27
|
+
cluster_name: Sequence[str] | None = None,
|
28
|
+
namespace_name: Sequence[str] | None = None,
|
29
|
+
thread_pool_size: int = 10,
|
30
|
+
use_jump_host: bool = True,
|
31
|
+
include_pattern: str | None = None,
|
32
|
+
exclude_pattern: str | None = None,
|
33
|
+
) -> list[dict[str, Any]]:
|
34
|
+
"""Gets all the images in the clusters/namespaces given. Returns a list of dicts
|
35
|
+
with the following keys:
|
36
|
+
* name: image name
|
37
|
+
* namespaces: a comma separated list of namespaces where the instance is used
|
38
|
+
* count: number of uses of the image
|
39
|
+
"""
|
40
|
+
all_namespaces = get_namespaces_minimal()
|
41
|
+
namespaces = filter_namespaces_by_cluster_and_namespace(
|
42
|
+
namespaces=all_namespaces,
|
43
|
+
cluster_names=cluster_name,
|
44
|
+
namespace_names=namespace_name,
|
45
|
+
)
|
46
|
+
vault_settings = get_app_interface_vault_settings()
|
47
|
+
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
48
|
+
oc_map = init_oc_map_from_namespaces(
|
49
|
+
namespaces=namespaces,
|
50
|
+
integration="qontract-cli-get-namespace_images",
|
51
|
+
secret_reader=secret_reader,
|
52
|
+
use_jump_host=use_jump_host,
|
53
|
+
thread_pool_size=thread_pool_size,
|
54
|
+
init_projects=True,
|
55
|
+
)
|
56
|
+
|
57
|
+
return fetch_pods_images_from_namespaces(
|
58
|
+
namespaces=namespaces,
|
59
|
+
oc_map=oc_map,
|
60
|
+
exclude_pattern=exclude_pattern,
|
61
|
+
include_pattern=include_pattern,
|
62
|
+
thread_pool_size=thread_pool_size,
|
63
|
+
)
|
64
|
+
|
65
|
+
|
66
|
+
def fetch_pods_images_from_namespaces(
|
67
|
+
namespaces: list[NamespaceV1],
|
68
|
+
oc_map: OCMap,
|
69
|
+
include_pattern: str | None = None,
|
70
|
+
exclude_pattern: str | None = None,
|
71
|
+
thread_pool_size: int = 10,
|
72
|
+
) -> list[dict[str, Any]]:
|
73
|
+
all_namespace_images = threaded.run(
|
74
|
+
func=_get_namespace_images,
|
75
|
+
iterable=namespaces,
|
76
|
+
thread_pool_size=thread_pool_size,
|
77
|
+
oc_map=oc_map,
|
78
|
+
)
|
79
|
+
|
80
|
+
result: defaultdict = defaultdict(_get_all_images_default)
|
81
|
+
for ni in all_namespace_images:
|
82
|
+
for name in ni.image_names:
|
83
|
+
result[name]["namespaces"].add(ni.namespace_name)
|
84
|
+
result[name]["count"] += 1
|
85
|
+
|
86
|
+
exclude_pattern_compiled: re.Pattern | None = None
|
87
|
+
if exclude_pattern:
|
88
|
+
exclude_pattern_compiled = re.compile(exclude_pattern)
|
89
|
+
|
90
|
+
include_pattern_compiled: re.Pattern | None = None
|
91
|
+
if include_pattern:
|
92
|
+
include_pattern_compiled = re.compile(include_pattern)
|
93
|
+
|
94
|
+
result_filtered_flattened: list[dict[str, Any]] = []
|
95
|
+
for name, value in result.items():
|
96
|
+
if include_pattern_compiled and not include_pattern_compiled.match(name):
|
97
|
+
continue
|
98
|
+
if exclude_pattern_compiled and exclude_pattern_compiled.match(name):
|
99
|
+
continue
|
100
|
+
|
101
|
+
result_filtered_flattened.append({
|
102
|
+
"name": name,
|
103
|
+
"namespaces": ",".join(sorted(value["namespaces"])),
|
104
|
+
"count": value["count"],
|
105
|
+
})
|
106
|
+
|
107
|
+
return result_filtered_flattened
|
108
|
+
|
109
|
+
|
110
|
+
def _get_all_images_default() -> dict[str, Any]:
|
111
|
+
return {"namespaces": set(), "count": 0}
|
112
|
+
|
113
|
+
|
114
|
+
def _get_namespace_images(ns: NamespaceV1, oc_map: OCMap) -> NamespaceImages:
|
115
|
+
image_names = []
|
116
|
+
oc = oc_map.get_cluster(ns.cluster.name)
|
117
|
+
pod_items = oc.get_items("Pod", namespace=ns.name)
|
118
|
+
for pod in pod_items:
|
119
|
+
containers = pod.get("spec", {}).get("containers", [])
|
120
|
+
containers.extend(pod.get("spec", {}).get("initContainers", []))
|
121
|
+
|
122
|
+
for c in containers:
|
123
|
+
if m := IMAGE_NAME_REGEX.match(c["image"]):
|
124
|
+
image_names.append(m.group("name"))
|
125
|
+
|
126
|
+
return NamespaceImages(
|
127
|
+
namespace_name=ns.name,
|
128
|
+
image_names=image_names,
|
129
|
+
)
|
tools/qontract_cli.py
CHANGED
@@ -63,9 +63,14 @@ from reconcile.checkpoint import report_invalid_metadata
|
|
63
63
|
from reconcile.cli import (
|
64
64
|
TERRAFORM_VERSION,
|
65
65
|
TERRAFORM_VERSION_REGEX,
|
66
|
+
cluster_name,
|
66
67
|
config_file,
|
68
|
+
namespace_name,
|
67
69
|
use_jump_host,
|
68
70
|
)
|
71
|
+
from reconcile.cli import (
|
72
|
+
threaded as thread_pool_size,
|
73
|
+
)
|
69
74
|
from reconcile.gql_definitions.advanced_upgrade_service.aus_clusters import (
|
70
75
|
query as aus_clusters_query,
|
71
76
|
)
|
@@ -136,7 +141,9 @@ from reconcile.utils.oc import (
|
|
136
141
|
OC_Map,
|
137
142
|
OCLogMsg,
|
138
143
|
)
|
139
|
-
from reconcile.utils.oc_map import
|
144
|
+
from reconcile.utils.oc_map import (
|
145
|
+
init_oc_map_from_clusters,
|
146
|
+
)
|
140
147
|
from reconcile.utils.ocm import OCM_PRODUCT_ROSA, OCMMap
|
141
148
|
from reconcile.utils.ocm_base_client import init_ocm_base_client
|
142
149
|
from reconcile.utils.output import print_output
|
@@ -4313,5 +4320,68 @@ def migrate(ctx, dry_run: bool, skip_build: bool) -> None:
|
|
4313
4320
|
rich_print(f"[b red]Please remove the temporary directory ({tempdir}) manually!")
|
4314
4321
|
|
4315
4322
|
|
4323
|
+
@get.command(help="Get all container images in app-interface defined namespaces")
|
4324
|
+
@cluster_name
|
4325
|
+
@namespace_name
|
4326
|
+
@thread_pool_size()
|
4327
|
+
@use_jump_host()
|
4328
|
+
@click.option("--exclude-pattern", help="Exclude images that match this pattern")
|
4329
|
+
@click.option("--include-pattern", help="Only include images that match this pattern")
|
4330
|
+
@click.pass_context
|
4331
|
+
def container_images(
|
4332
|
+
ctx,
|
4333
|
+
cluster_name,
|
4334
|
+
namespace_name,
|
4335
|
+
thread_pool_size,
|
4336
|
+
use_jump_host,
|
4337
|
+
exclude_pattern,
|
4338
|
+
include_pattern,
|
4339
|
+
):
|
4340
|
+
from tools.cli_commands.container_images_report import get_all_pods_images
|
4341
|
+
|
4342
|
+
results = get_all_pods_images(
|
4343
|
+
cluster_name=cluster_name,
|
4344
|
+
namespace_name=namespace_name,
|
4345
|
+
thread_pool_size=thread_pool_size,
|
4346
|
+
use_jump_host=use_jump_host,
|
4347
|
+
exclude_pattern=exclude_pattern,
|
4348
|
+
include_pattern=include_pattern,
|
4349
|
+
)
|
4350
|
+
|
4351
|
+
if ctx.obj["options"]["output"] == "md":
|
4352
|
+
json_table = {
|
4353
|
+
"filter": True,
|
4354
|
+
"fields": [
|
4355
|
+
{"key": "name", "sortable": True},
|
4356
|
+
{"key": "namespaces", "sortable": True},
|
4357
|
+
{"key": "count", "sortable": True},
|
4358
|
+
],
|
4359
|
+
"items": results,
|
4360
|
+
}
|
4361
|
+
|
4362
|
+
print(
|
4363
|
+
f"""
|
4364
|
+
You can view the source of this Markdown to extract the JSON data.
|
4365
|
+
|
4366
|
+
{len(results)} container images found.
|
4367
|
+
|
4368
|
+
exclude-pattern = {exclude_pattern}
|
4369
|
+
include-pattern = {include_pattern}
|
4370
|
+
|
4371
|
+
```json:table
|
4372
|
+
{json.dumps(json_table)}
|
4373
|
+
```
|
4374
|
+
"""
|
4375
|
+
)
|
4376
|
+
else:
|
4377
|
+
columns = [
|
4378
|
+
"name",
|
4379
|
+
"namespaces",
|
4380
|
+
"count",
|
4381
|
+
]
|
4382
|
+
ctx.obj["options"]["sort"] = False
|
4383
|
+
print_output(ctx.obj["options"], results, columns)
|
4384
|
+
|
4385
|
+
|
4316
4386
|
if __name__ == "__main__":
|
4317
4387
|
root() # pylint: disable=no-value-for-parameter
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import pytest
|
2
|
+
from pytest_mock import MockerFixture
|
3
|
+
|
4
|
+
from reconcile.gql_definitions.common.namespaces_minimal import ClusterV1, NamespaceV1
|
5
|
+
from reconcile.gql_definitions.fragments.vault_secret import VaultSecret
|
6
|
+
from reconcile.test.fixtures import Fixtures
|
7
|
+
from reconcile.utils.oc import OCNative
|
8
|
+
from reconcile.utils.oc_map import OCMap
|
9
|
+
from tools.cli_commands.container_images_report import (
|
10
|
+
fetch_pods_images_from_namespaces,
|
11
|
+
)
|
12
|
+
|
13
|
+
fxt = Fixtures("container_images_report")
|
14
|
+
|
15
|
+
|
16
|
+
@pytest.fixture
|
17
|
+
def observability_pods() -> list[dict]:
|
18
|
+
return fxt.get_anymarkup("app-sre-observability-stage-pods.yaml")
|
19
|
+
|
20
|
+
|
21
|
+
@pytest.fixture
|
22
|
+
def pipeline_pods() -> list[dict]:
|
23
|
+
return fxt.get_anymarkup("app-sre-pipelines-pods.yaml")
|
24
|
+
|
25
|
+
|
26
|
+
@pytest.fixture
|
27
|
+
def namespaces() -> list[NamespaceV1]:
|
28
|
+
return [
|
29
|
+
NamespaceV1(
|
30
|
+
name="app-sre-observability-stage",
|
31
|
+
delete=None,
|
32
|
+
labels="{}",
|
33
|
+
clusterAdmin=None,
|
34
|
+
cluster=ClusterV1(
|
35
|
+
name="appsres09ue1",
|
36
|
+
serverUrl="https://api.appsres09ue1.24ep.p3.openshiftapps.com:443",
|
37
|
+
insecureSkipTLSVerify=None,
|
38
|
+
jumpHost=None,
|
39
|
+
automationToken=VaultSecret(
|
40
|
+
path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1",
|
41
|
+
field="token",
|
42
|
+
version=None,
|
43
|
+
format=None,
|
44
|
+
),
|
45
|
+
clusterAdminAutomationToken=VaultSecret(
|
46
|
+
path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1-cluster-admin",
|
47
|
+
field="token",
|
48
|
+
version=None,
|
49
|
+
format=None,
|
50
|
+
),
|
51
|
+
internal=True,
|
52
|
+
disable=None,
|
53
|
+
),
|
54
|
+
),
|
55
|
+
NamespaceV1(
|
56
|
+
name="app-sre-pipelines",
|
57
|
+
delete=None,
|
58
|
+
labels='{"provider": "tekton"}',
|
59
|
+
clusterAdmin=None,
|
60
|
+
cluster=ClusterV1(
|
61
|
+
name="appsres09ue1",
|
62
|
+
serverUrl="https://api.appsres09ue1.24ep.p3.openshiftapps.com:443",
|
63
|
+
insecureSkipTLSVerify=None,
|
64
|
+
jumpHost=None,
|
65
|
+
automationToken=VaultSecret(
|
66
|
+
path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1",
|
67
|
+
field="token",
|
68
|
+
version=None,
|
69
|
+
format=None,
|
70
|
+
),
|
71
|
+
clusterAdminAutomationToken=VaultSecret(
|
72
|
+
path="app-sre/integrations-output/openshift-cluster-bots/appsres09ue1-cluster-admin",
|
73
|
+
field="token",
|
74
|
+
version=None,
|
75
|
+
format=None,
|
76
|
+
),
|
77
|
+
internal=True,
|
78
|
+
disable=None,
|
79
|
+
),
|
80
|
+
),
|
81
|
+
]
|
82
|
+
|
83
|
+
|
84
|
+
@pytest.fixture
|
85
|
+
def oc(
|
86
|
+
mocker: MockerFixture,
|
87
|
+
observability_pods: list[dict],
|
88
|
+
pipeline_pods: list[dict],
|
89
|
+
) -> OCNative:
|
90
|
+
oc = mocker.patch("reconcile.utils.oc.OCNative", autospec=True)
|
91
|
+
oc.get_items.side_effect = [observability_pods, pipeline_pods]
|
92
|
+
return oc
|
93
|
+
|
94
|
+
|
95
|
+
@pytest.fixture
|
96
|
+
def oc_map(mocker: MockerFixture, oc: OCNative) -> OCMap:
|
97
|
+
oc_map = mocker.patch("reconcile.utils.oc_map.OCMap", autospec=True)
|
98
|
+
oc_map.get_cluster.return_value = oc
|
99
|
+
return oc_map
|
100
|
+
|
101
|
+
|
102
|
+
# convert a list of dicts into a set of tuples to use it in assertions
|
103
|
+
def _to_set(list_of_dicts: list[dict]) -> set[tuple]:
|
104
|
+
return {tuple(d.items()) for d in list_of_dicts}
|
105
|
+
|
106
|
+
|
107
|
+
def testfetch_no_filter(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
|
108
|
+
images = fetch_pods_images_from_namespaces(
|
109
|
+
namespaces=namespaces,
|
110
|
+
oc_map=oc_map,
|
111
|
+
thread_pool_size=2,
|
112
|
+
)
|
113
|
+
|
114
|
+
assert _to_set(images) == _to_set([
|
115
|
+
{
|
116
|
+
"name": "quay.io/prometheus/blackbox-exporter",
|
117
|
+
"namespaces": "app-sre-observability-stage",
|
118
|
+
"count": 1,
|
119
|
+
},
|
120
|
+
{
|
121
|
+
"name": "quay.io/redhat-services-prod/app-sre-tenant/gitlab-project-exporter-main/gitlab-project-exporter-main",
|
122
|
+
"namespaces": "app-sre-observability-stage",
|
123
|
+
"count": 1,
|
124
|
+
},
|
125
|
+
{
|
126
|
+
"name": "quay.io/app-sre/internal-redhat-ca",
|
127
|
+
"namespaces": "app-sre-observability-stage,app-sre-pipelines",
|
128
|
+
"count": 3,
|
129
|
+
},
|
130
|
+
{
|
131
|
+
"name": "quay.io/app-sre/clamav",
|
132
|
+
"namespaces": "app-sre-pipelines",
|
133
|
+
"count": 1,
|
134
|
+
},
|
135
|
+
{
|
136
|
+
"name": "quay.io/redhat-appstudio/clamav-db",
|
137
|
+
"namespaces": "app-sre-pipelines",
|
138
|
+
"count": 1,
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
|
142
|
+
"namespaces": "app-sre-pipelines",
|
143
|
+
"count": 3,
|
144
|
+
},
|
145
|
+
{
|
146
|
+
"name": "quay.io/redhatproductsecurity/rapidast",
|
147
|
+
"namespaces": "app-sre-pipelines",
|
148
|
+
"count": 1,
|
149
|
+
},
|
150
|
+
])
|
151
|
+
|
152
|
+
|
153
|
+
def testfetch_exclude_pattern(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
|
154
|
+
images = fetch_pods_images_from_namespaces(
|
155
|
+
namespaces=namespaces,
|
156
|
+
oc_map=oc_map,
|
157
|
+
thread_pool_size=2,
|
158
|
+
exclude_pattern="quay.io/redhat|quay.io/app-sre",
|
159
|
+
)
|
160
|
+
assert _to_set(images) == _to_set([
|
161
|
+
{
|
162
|
+
"name": "quay.io/prometheus/blackbox-exporter",
|
163
|
+
"namespaces": "app-sre-observability-stage",
|
164
|
+
"count": 1,
|
165
|
+
},
|
166
|
+
{
|
167
|
+
"name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
|
168
|
+
"namespaces": "app-sre-pipelines",
|
169
|
+
"count": 3,
|
170
|
+
},
|
171
|
+
])
|
172
|
+
|
173
|
+
|
174
|
+
def testfetch_include_pattern(namespaces: list[NamespaceV1], oc_map: OCMap) -> None:
|
175
|
+
images = fetch_pods_images_from_namespaces(
|
176
|
+
namespaces=namespaces,
|
177
|
+
oc_map=oc_map,
|
178
|
+
thread_pool_size=2,
|
179
|
+
include_pattern="^registry.redhat.io",
|
180
|
+
)
|
181
|
+
assert images == [
|
182
|
+
{
|
183
|
+
"name": "registry.redhat.io/openshift-pipelines/pipelines-entrypoint-rhel8",
|
184
|
+
"namespaces": "app-sre-pipelines",
|
185
|
+
"count": 3,
|
186
|
+
},
|
187
|
+
]
|
{qontract_reconcile-0.10.1rc1180.dist-info → qontract_reconcile-0.10.1rc1182.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|