gitlabform 0.0.540a0__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.
- gitlabform/__init__.py +719 -0
- gitlabform/configuration/__init__.py +12 -0
- gitlabform/configuration/common.py +19 -0
- gitlabform/configuration/core.py +323 -0
- gitlabform/configuration/groups.py +127 -0
- gitlabform/configuration/projects.py +73 -0
- gitlabform/configuration/transform.py +259 -0
- gitlabform/constants.py +7 -0
- gitlabform/gitlab/__init__.py +108 -0
- gitlabform/gitlab/commits.py +39 -0
- gitlabform/gitlab/core.py +334 -0
- gitlabform/gitlab/group_badges.py +50 -0
- gitlabform/gitlab/group_ldap_links.py +40 -0
- gitlabform/gitlab/groups.py +96 -0
- gitlabform/gitlab/merge_requests.py +57 -0
- gitlabform/gitlab/pipelines.py +23 -0
- gitlabform/gitlab/project_badges.py +52 -0
- gitlabform/gitlab/project_deploy_keys.py +102 -0
- gitlabform/gitlab/project_merge_requests_approvals.py +94 -0
- gitlabform/gitlab/project_protected_environments.py +37 -0
- gitlabform/gitlab/projects.py +151 -0
- gitlabform/gitlab/python_gitlab.py +251 -0
- gitlabform/gitlab/variables.py +47 -0
- gitlabform/lists/__init__.py +62 -0
- gitlabform/lists/filter.py +99 -0
- gitlabform/lists/groups.py +87 -0
- gitlabform/lists/projects.py +239 -0
- gitlabform/output.py +46 -0
- gitlabform/processors/__init__.py +43 -0
- gitlabform/processors/abstract_processor.py +187 -0
- gitlabform/processors/application/__init__.py +17 -0
- gitlabform/processors/application/application_settings_processor.py +39 -0
- gitlabform/processors/defining_keys.py +152 -0
- gitlabform/processors/group/__init__.py +48 -0
- gitlabform/processors/group/group_badges_processor.py +17 -0
- gitlabform/processors/group/group_hooks_processor.py +75 -0
- gitlabform/processors/group/group_labels_processor.py +28 -0
- gitlabform/processors/group/group_ldap_links_processor.py +16 -0
- gitlabform/processors/group/group_members_processor.py +287 -0
- gitlabform/processors/group/group_push_rules_processor.py +44 -0
- gitlabform/processors/group/group_saml_links_processor.py +65 -0
- gitlabform/processors/group/group_settings_processor.py +90 -0
- gitlabform/processors/group/group_variables_processor.py +26 -0
- gitlabform/processors/multiple_entities_processor.py +171 -0
- gitlabform/processors/project/__init__.py +80 -0
- gitlabform/processors/project/badges_processor.py +17 -0
- gitlabform/processors/project/branches_processor.py +514 -0
- gitlabform/processors/project/deploy_keys_processor.py +18 -0
- gitlabform/processors/project/files_processor.py +301 -0
- gitlabform/processors/project/hooks_processor.py +64 -0
- gitlabform/processors/project/integrations_processor.py +33 -0
- gitlabform/processors/project/job_token_scope_processor.py +216 -0
- gitlabform/processors/project/members_processor.py +204 -0
- gitlabform/processors/project/merge_requests_approval_rules.py +17 -0
- gitlabform/processors/project/merge_requests_approvals.py +59 -0
- gitlabform/processors/project/project_labels_processor.py +27 -0
- gitlabform/processors/project/project_processor.py +62 -0
- gitlabform/processors/project/project_push_rules_processor.py +52 -0
- gitlabform/processors/project/project_security_settings.py +66 -0
- gitlabform/processors/project/project_settings_processor.py +239 -0
- gitlabform/processors/project/project_variables_processor.py +94 -0
- gitlabform/processors/project/remote_mirrors_processor.py +278 -0
- gitlabform/processors/project/resource_groups_processor.py +48 -0
- gitlabform/processors/project/schedules_processor.py +208 -0
- gitlabform/processors/project/tags_processor.py +108 -0
- gitlabform/processors/shared/__init__.py +0 -0
- gitlabform/processors/shared/protected_environments_processor.py +20 -0
- gitlabform/processors/util/__init__.py +0 -0
- gitlabform/processors/util/decorators.py +44 -0
- gitlabform/processors/util/difference_logger.py +70 -0
- gitlabform/processors/util/labels_processor.py +120 -0
- gitlabform/processors/util/variables_processor.py +143 -0
- gitlabform/run.py +9 -0
- gitlabform/util.py +7 -0
- gitlabform-0.0.540a0.dist-info/METADATA +54 -0
- gitlabform-0.0.540a0.dist-info/RECORD +79 -0
- gitlabform-0.0.540a0.dist-info/WHEEL +4 -0
- gitlabform-0.0.540a0.dist-info/entry_points.txt +9 -0
- gitlabform-0.0.540a0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Dict, Tuple
|
|
3
|
+
|
|
4
|
+
from logging import debug, info, critical, error
|
|
5
|
+
|
|
6
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
7
|
+
from gitlabform.gitlab import GitLab, AccessLevel
|
|
8
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
9
|
+
from gitlab.v4.objects import Group, GroupMember, User
|
|
10
|
+
from gitlab import GitlabDeleteError, GitlabError, GitlabGetError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GroupMembersProcessor(AbstractProcessor):
|
|
14
|
+
def __init__(self, gitlab: GitLab):
|
|
15
|
+
super().__init__("group_members", gitlab)
|
|
16
|
+
|
|
17
|
+
def _process_configuration(self, group_name: str, configuration: dict):
|
|
18
|
+
keep_bots = configuration.get("group_members|keep_bots", False)
|
|
19
|
+
|
|
20
|
+
enforce_group_members = configuration.get("group_members|enforce", False)
|
|
21
|
+
|
|
22
|
+
(
|
|
23
|
+
groups_to_set_by_group_path,
|
|
24
|
+
users_to_set_by_username,
|
|
25
|
+
) = self._get_groups_and_users_to_set(configuration)
|
|
26
|
+
|
|
27
|
+
if enforce_group_members and not groups_to_set_by_group_path and not users_to_set_by_username:
|
|
28
|
+
critical(
|
|
29
|
+
"Group members configuration section has to contain"
|
|
30
|
+
" some 'users' or 'groups' defined as Owners,"
|
|
31
|
+
" if you want to enforce them (GitLab requires it)."
|
|
32
|
+
)
|
|
33
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
34
|
+
|
|
35
|
+
group = self.gl.get_group_by_path_cached(group_name)
|
|
36
|
+
|
|
37
|
+
self._process_groups(group, groups_to_set_by_group_path, enforce_group_members)
|
|
38
|
+
|
|
39
|
+
self._process_users(
|
|
40
|
+
users_to_set_by_username,
|
|
41
|
+
enforce_group_members,
|
|
42
|
+
keep_bots,
|
|
43
|
+
group,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _get_groups_and_users_to_set(configuration: dict) -> Tuple[dict, dict]:
|
|
48
|
+
groups_to_set_by_group_path = configuration.get("group_members|groups", {})
|
|
49
|
+
|
|
50
|
+
users_to_set_by_username = configuration.get("group_members", {})
|
|
51
|
+
if users_to_set_by_username:
|
|
52
|
+
proper_users_to_set_by_username = configuration.get("group_members|users", {})
|
|
53
|
+
if proper_users_to_set_by_username:
|
|
54
|
+
users_to_set_by_username = proper_users_to_set_by_username
|
|
55
|
+
else:
|
|
56
|
+
users_to_set_by_username.pop("enforce", None)
|
|
57
|
+
users_to_set_by_username.pop("users", None)
|
|
58
|
+
users_to_set_by_username.pop("groups", None)
|
|
59
|
+
users_to_set_by_username.pop("keep_bots", None)
|
|
60
|
+
|
|
61
|
+
return groups_to_set_by_group_path, users_to_set_by_username
|
|
62
|
+
|
|
63
|
+
def _process_groups(
|
|
64
|
+
self,
|
|
65
|
+
group_being_processed: Group,
|
|
66
|
+
groups_to_share_with_by_path: dict,
|
|
67
|
+
enforce_group_members: bool,
|
|
68
|
+
):
|
|
69
|
+
shared_with_groups_before = group_being_processed.shared_with_groups
|
|
70
|
+
info("Group shared with BEFORE: %s", shared_with_groups_before)
|
|
71
|
+
|
|
72
|
+
groups_before_by_group_path = dict()
|
|
73
|
+
for shared_with_group in shared_with_groups_before:
|
|
74
|
+
groups_before_by_group_path[shared_with_group["group_full_path"]] = shared_with_group
|
|
75
|
+
|
|
76
|
+
for share_with_group_path in groups_to_share_with_by_path:
|
|
77
|
+
group_access_to_set = groups_to_share_with_by_path[share_with_group_path]["group_access"]
|
|
78
|
+
|
|
79
|
+
expires_at_to_set = (
|
|
80
|
+
groups_to_share_with_by_path[share_with_group_path]["expires_at"]
|
|
81
|
+
if "expires_at" in groups_to_share_with_by_path[share_with_group_path]
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if share_with_group_path in groups_before_by_group_path:
|
|
86
|
+
group_access_before = groups_before_by_group_path[share_with_group_path]["group_access_level"]
|
|
87
|
+
expires_at_before = groups_before_by_group_path[share_with_group_path]["expires_at"]
|
|
88
|
+
|
|
89
|
+
if group_access_before == group_access_to_set and expires_at_before == expires_at_to_set:
|
|
90
|
+
info(
|
|
91
|
+
"Nothing to change for group '%s' - same config now as to set.",
|
|
92
|
+
share_with_group_path,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
info(f"Re-adding group {share_with_group_path} to change their access level or expires at.")
|
|
96
|
+
share_with_group_id = groups_before_by_group_path[share_with_group_path]["group_id"]
|
|
97
|
+
# we will remove the group first and then re-add them,
|
|
98
|
+
# to ensure that the group has the expected access level
|
|
99
|
+
self._unshare(group_being_processed, share_with_group_id)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
|
|
103
|
+
except GitlabError as e:
|
|
104
|
+
error(f"Error processing {share_with_group_path}, {e.error_message}")
|
|
105
|
+
raise e
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
info(
|
|
109
|
+
f"Adding group {share_with_group_path} who previously was not a member.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
share_with_group_id = self.gl.get_group_id(share_with_group_path)
|
|
113
|
+
try:
|
|
114
|
+
group_being_processed.share(share_with_group_id, group_access_to_set, expires_at_to_set)
|
|
115
|
+
except GitlabError as e:
|
|
116
|
+
error(f"Error processing {share_with_group_path}, {e.error_message}")
|
|
117
|
+
raise e
|
|
118
|
+
|
|
119
|
+
if enforce_group_members:
|
|
120
|
+
# remove groups not configured explicitly
|
|
121
|
+
groups_not_configured = set(groups_before_by_group_path) - set(groups_to_share_with_by_path)
|
|
122
|
+
for group_path in groups_not_configured:
|
|
123
|
+
info(
|
|
124
|
+
"Removing group '%s' who is not configured to be a member.",
|
|
125
|
+
group_path,
|
|
126
|
+
)
|
|
127
|
+
share_with_group_id = self.gl.get_group_id(group_path)
|
|
128
|
+
self._unshare(group_being_processed, share_with_group_id)
|
|
129
|
+
else:
|
|
130
|
+
info("Not enforcing group members.")
|
|
131
|
+
|
|
132
|
+
info(
|
|
133
|
+
"Group shared with AFTER: %s",
|
|
134
|
+
group_being_processed.members.list(get_all=True),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _unshare(group_being_processed, share_with_group_id):
|
|
139
|
+
try:
|
|
140
|
+
group_being_processed.unshare(share_with_group_id)
|
|
141
|
+
except GitlabDeleteError:
|
|
142
|
+
info(f"Group with id {share_with_group_id} could not be unshared, likely was never shared to begin with")
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def _process_users(
|
|
146
|
+
self,
|
|
147
|
+
users_to_set_by_username: dict,
|
|
148
|
+
enforce_group_members: bool,
|
|
149
|
+
keep_bots: bool,
|
|
150
|
+
group: Group,
|
|
151
|
+
):
|
|
152
|
+
# group users before by username
|
|
153
|
+
# (note: we DON'T get inherited users as we don't manage them at this level anyway)
|
|
154
|
+
users_before = self.get_group_members(group)
|
|
155
|
+
|
|
156
|
+
info("Group members BEFORE: %s", users_before.keys())
|
|
157
|
+
|
|
158
|
+
if users_to_set_by_username:
|
|
159
|
+
# group users to set by access level
|
|
160
|
+
users_to_set_by_access_level: Dict[int, list] = dict()
|
|
161
|
+
for user in users_to_set_by_username:
|
|
162
|
+
access_level = users_to_set_by_username[user]["access_level"]
|
|
163
|
+
users_to_set_by_access_level.setdefault(access_level, []).append(user)
|
|
164
|
+
|
|
165
|
+
# we HAVE TO start configuring access from the highest access level - in case of groups this is Owner
|
|
166
|
+
# - to ensure that we won't end up with no Owner in a group
|
|
167
|
+
for level in reversed(sorted(AccessLevel.group_levels())):
|
|
168
|
+
users_to_set_with_this_level = (
|
|
169
|
+
users_to_set_by_access_level[level] if level in users_to_set_by_access_level else []
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for user in users_to_set_with_this_level:
|
|
173
|
+
access_level_to_set = users_to_set_by_username[user]["access_level"]
|
|
174
|
+
expires_at_to_set = (
|
|
175
|
+
users_to_set_by_username[user]["expires_at"]
|
|
176
|
+
if "expires_at" in users_to_set_by_username[user]
|
|
177
|
+
else None
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
member_role_id_or_name = (
|
|
181
|
+
users_to_set_by_username[user]["member_role"]
|
|
182
|
+
if "member_role" in users_to_set_by_username[user]
|
|
183
|
+
else None
|
|
184
|
+
)
|
|
185
|
+
if member_role_id_or_name:
|
|
186
|
+
member_role_id_to_set = self.gl.get_member_role_id_cached(
|
|
187
|
+
member_role_id_or_name, group.full_path
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
member_role_id_to_set = None
|
|
191
|
+
|
|
192
|
+
common_username = user.lower()
|
|
193
|
+
|
|
194
|
+
user_id = self.gl.get_user_id_cached(user)
|
|
195
|
+
if user_id is None:
|
|
196
|
+
message = f"Could not find User '{user}' on the Instance"
|
|
197
|
+
error(message)
|
|
198
|
+
raise GitlabGetError(message, 404)
|
|
199
|
+
|
|
200
|
+
if common_username in users_before:
|
|
201
|
+
group_member: GroupMember = group.members.get(user_id)
|
|
202
|
+
|
|
203
|
+
user_before = users_before[common_username]
|
|
204
|
+
access_level_before = user_before.access_level
|
|
205
|
+
expires_at_before = user_before.expires_at
|
|
206
|
+
if hasattr(user_before, "member_role"):
|
|
207
|
+
member_role_id_before = user_before.member_role["id"]
|
|
208
|
+
else:
|
|
209
|
+
member_role_id_before = None
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
access_level_before == access_level_to_set
|
|
213
|
+
and expires_at_before == expires_at_to_set
|
|
214
|
+
and member_role_id_before == member_role_id_to_set
|
|
215
|
+
):
|
|
216
|
+
info(
|
|
217
|
+
"Nothing to change for user '%s' - same config now as to set.",
|
|
218
|
+
common_username,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
info(
|
|
222
|
+
f"Editing user {common_username} to change their access level to {access_level_to_set},"
|
|
223
|
+
f" expires at to {expires_at_to_set},"
|
|
224
|
+
f" and member_role_id to {member_role_id_to_set}."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
group_member.access_level = access_level_to_set
|
|
228
|
+
group_member.expires_at = expires_at_to_set
|
|
229
|
+
group_member.member_role_id = member_role_id_to_set
|
|
230
|
+
try:
|
|
231
|
+
group_member.save()
|
|
232
|
+
except GitlabError as e:
|
|
233
|
+
error(f"Could not save user {common_username}, error: {e.error_message}")
|
|
234
|
+
raise e
|
|
235
|
+
|
|
236
|
+
else:
|
|
237
|
+
info(f"Adding user {common_username} who previously was not a member.")
|
|
238
|
+
group.members.create(
|
|
239
|
+
{
|
|
240
|
+
"user_id": user_id,
|
|
241
|
+
"access_level": access_level_to_set,
|
|
242
|
+
"expires_at": expires_at_to_set,
|
|
243
|
+
"member_role_id": member_role_id_to_set,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if enforce_group_members:
|
|
248
|
+
# remove users not configured explicitly
|
|
249
|
+
# note: only direct members are removed - inherited are left
|
|
250
|
+
users_not_configured = set(users_before.keys()) - set(
|
|
251
|
+
[username.lower() for username in users_to_set_by_username.keys()]
|
|
252
|
+
)
|
|
253
|
+
for user in users_not_configured:
|
|
254
|
+
info(f"Removing user {user} who is not configured to be a member.")
|
|
255
|
+
|
|
256
|
+
gl_user: User | None = self.gl.get_user_by_username_cached(user)
|
|
257
|
+
|
|
258
|
+
if gl_user is None:
|
|
259
|
+
# User does not exist an instance level but is for whatever reason present on a Group/Project
|
|
260
|
+
# We should raise error into Logs but not prevent the rest of GitLabForm from executing
|
|
261
|
+
# This error is more likely to be prevalent in Dedicated instances; it is unlikely for a User to
|
|
262
|
+
# be completely deleted from gitlab.com
|
|
263
|
+
error(f"Could not find User '{user}' on the Instance so can not remove User from Group")
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if keep_bots and gl_user.bot:
|
|
267
|
+
info(f"Will not remove bot user '{user}' as the 'keep_bots' option is true.")
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
group.members.delete(gl_user.id)
|
|
272
|
+
except GitlabDeleteError as delete_error:
|
|
273
|
+
error(f"Member '{user}' could not be deleted: {delete_error}")
|
|
274
|
+
raise delete_error
|
|
275
|
+
|
|
276
|
+
else:
|
|
277
|
+
info("Not enforcing group members.")
|
|
278
|
+
|
|
279
|
+
info(f"Group members AFTER: {group.members.list(get_all=True)}")
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def get_group_members(group) -> dict:
|
|
283
|
+
members = group.members.list(get_all=True)
|
|
284
|
+
users = {}
|
|
285
|
+
for member in members:
|
|
286
|
+
users[member.username.lower()] = member
|
|
287
|
+
return users
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from logging import info, debug
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from gitlabform.gitlab import GitLab
|
|
5
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
6
|
+
|
|
7
|
+
from gitlab.v4.objects.groups import Group
|
|
8
|
+
from gitlab.exceptions import GitlabGetError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GroupPushRulesProcessor(AbstractProcessor):
|
|
12
|
+
def __init__(self, gitlab: GitLab):
|
|
13
|
+
super().__init__("group_push_rules", gitlab)
|
|
14
|
+
|
|
15
|
+
def _process_configuration(self, group: str, configuration: Dict):
|
|
16
|
+
configured_group_push_rules = configuration.get("group_push_rules", {})
|
|
17
|
+
|
|
18
|
+
gitlab_group: Group = self.gl.get_group_by_path_cached(group)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
existing_push_rules = gitlab_group.pushrules.get()
|
|
22
|
+
except GitlabGetError as e:
|
|
23
|
+
if e.response_code == 404:
|
|
24
|
+
debug(f"No existing push rules for {gitlab_group.name}, creating new push rules.")
|
|
25
|
+
self.create_group_push_rules(gitlab_group, configured_group_push_rules)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if self._needs_update(existing_push_rules.asdict(), configured_group_push_rules):
|
|
29
|
+
debug(f"Updating group push rules for group {gitlab_group.name}")
|
|
30
|
+
self.update_group_push_rules(existing_push_rules, configured_group_push_rules)
|
|
31
|
+
else:
|
|
32
|
+
debug("No update needed for Group Push Rules")
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def update_group_push_rules(push_rules, configured_group_push_rules: dict):
|
|
36
|
+
for key, value in configured_group_push_rules.items():
|
|
37
|
+
debug(f"Updating setting {key} to value {value}")
|
|
38
|
+
setattr(push_rules, key, value)
|
|
39
|
+
push_rules.save()
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def create_group_push_rules(gitlab_group, push_rules_config: Dict):
|
|
43
|
+
debug(f"Creating push rules with configuration: {push_rules_config}")
|
|
44
|
+
gitlab_group.pushrules.create(push_rules_config)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from logging import debug
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from gitlabform.gitlab import GitLab
|
|
5
|
+
from gitlab.v4.objects import Group, GroupSAMLGroupLink
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GroupSAMLLinksProcessor(AbstractProcessor):
|
|
10
|
+
|
|
11
|
+
def __init__(self, gitlab: GitLab):
|
|
12
|
+
super().__init__("group_saml_links", gitlab)
|
|
13
|
+
|
|
14
|
+
def _process_configuration(self, group_path: str, configuration: dict) -> None:
|
|
15
|
+
"""Process the SAML links configuration for a group."""
|
|
16
|
+
|
|
17
|
+
configured_links = configuration.get("group_saml_links", {})
|
|
18
|
+
enforce_links = configuration.get("group_saml_links|enforce", False)
|
|
19
|
+
|
|
20
|
+
group: Group = self.gl.get_group_by_path_cached(group_path)
|
|
21
|
+
existing_links: List[GroupSAMLGroupLink] = group.saml_group_links.list(get_all=True)
|
|
22
|
+
existing_link_names = [existing_link.name for existing_link in existing_links]
|
|
23
|
+
|
|
24
|
+
# Remove 'enforce' key from the config so that it's not treated as a "link"
|
|
25
|
+
if enforce_links:
|
|
26
|
+
configured_links.pop("enforce")
|
|
27
|
+
|
|
28
|
+
# Process each configured SAML link
|
|
29
|
+
for _, link_configuration in configured_links.items():
|
|
30
|
+
saml_group_name = link_configuration.get("saml_group_name")
|
|
31
|
+
|
|
32
|
+
if saml_group_name not in existing_link_names:
|
|
33
|
+
# Create the saml link as it does not already exist
|
|
34
|
+
group.saml_group_links.create(link_configuration)
|
|
35
|
+
group.save()
|
|
36
|
+
else:
|
|
37
|
+
# Check if the existing link needs to be updated
|
|
38
|
+
# GitLab API does not provide an endpoint for updating SAML links
|
|
39
|
+
# If update required, we need to delete and recreate
|
|
40
|
+
existing_link_config = next((link for link in existing_links if link.name == saml_group_name), None)
|
|
41
|
+
if existing_link_config and self._needs_update(existing_link_config.asdict(), link_configuration):
|
|
42
|
+
debug(f"Updating SAML link: {saml_group_name} with {link_configuration}")
|
|
43
|
+
existing_link_config.delete()
|
|
44
|
+
group.saml_group_links.create(link_configuration)
|
|
45
|
+
group.save()
|
|
46
|
+
|
|
47
|
+
# Process enforce mode
|
|
48
|
+
if enforce_links:
|
|
49
|
+
self._delete_extra_links(group, existing_links, configured_links)
|
|
50
|
+
|
|
51
|
+
def _delete_extra_links(
|
|
52
|
+
self,
|
|
53
|
+
group: Group,
|
|
54
|
+
existing: List[GroupSAMLGroupLink],
|
|
55
|
+
configured: dict,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Delete any SAML links that are not in the configuration."""
|
|
58
|
+
known_names = [
|
|
59
|
+
common_name["saml_group_name"] for common_name in configured.values() if common_name != "enforce"
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for link in existing:
|
|
63
|
+
if link.name not in known_names:
|
|
64
|
+
debug(f"Deleting extra SAML link: {link.name}")
|
|
65
|
+
group.saml_group_links.delete(link.name)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from logging import info, debug, warning
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from gitlabform.gitlab import GitLab
|
|
6
|
+
from gitlab.v4.objects.groups import Group
|
|
7
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroupSettingsProcessor(AbstractProcessor):
|
|
11
|
+
def __init__(self, gitlab: GitLab):
|
|
12
|
+
super().__init__("group_settings", gitlab)
|
|
13
|
+
|
|
14
|
+
def _process_configuration(self, group: str, configuration: Dict):
|
|
15
|
+
configured_group_settings = configuration.get("group_settings", {})
|
|
16
|
+
|
|
17
|
+
gitlab_group: Group = self.gl.get_group_by_path_cached(group)
|
|
18
|
+
|
|
19
|
+
# Remove avatar from config to process it last
|
|
20
|
+
avatar_config = configured_group_settings.pop("avatar", None)
|
|
21
|
+
|
|
22
|
+
# Process other settings first
|
|
23
|
+
if self._needs_update(gitlab_group.asdict(), configured_group_settings):
|
|
24
|
+
info(f"Updating group settings for group {gitlab_group.name}")
|
|
25
|
+
self.update_group_settings(gitlab_group, configured_group_settings)
|
|
26
|
+
else:
|
|
27
|
+
debug("No update needed for Group Settings")
|
|
28
|
+
|
|
29
|
+
# Process avatar last - with error handling that doesn't stop execution
|
|
30
|
+
if avatar_config is not None:
|
|
31
|
+
try:
|
|
32
|
+
self._process_group_avatar(gitlab_group, {"avatar": avatar_config})
|
|
33
|
+
except Exception as e:
|
|
34
|
+
warning(f"Failed to process group avatar: {e}")
|
|
35
|
+
raise e
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def update_group_settings(gitlab_group: Group, group_settings_config: dict):
|
|
39
|
+
for key in group_settings_config:
|
|
40
|
+
value = group_settings_config[key]
|
|
41
|
+
debug(f"Updating setting {key} to value {value}")
|
|
42
|
+
gitlab_group.__setattr__(key, value)
|
|
43
|
+
gitlab_group.save()
|
|
44
|
+
|
|
45
|
+
def _process_group_avatar(self, gitlab_group: Group, group_settings_config: dict) -> None:
|
|
46
|
+
"""Process group avatar settings from configuration."""
|
|
47
|
+
debug("Processing group avatar configuration")
|
|
48
|
+
|
|
49
|
+
avatar_path = group_settings_config.get("avatar")
|
|
50
|
+
if avatar_path is None:
|
|
51
|
+
debug("No avatar configuration provided, skipping avatar processing")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
debug(f"Avatar configuration found: {avatar_path}")
|
|
55
|
+
|
|
56
|
+
# Check current avatar status
|
|
57
|
+
current_avatar = getattr(gitlab_group, "avatar_url", None)
|
|
58
|
+
|
|
59
|
+
if avatar_path == "":
|
|
60
|
+
# Want to remove avatar
|
|
61
|
+
if not current_avatar:
|
|
62
|
+
debug("Avatar already empty, no update needed")
|
|
63
|
+
return
|
|
64
|
+
debug("Deleting group avatar")
|
|
65
|
+
gitlab_group.avatar = ""
|
|
66
|
+
gitlab_group.save()
|
|
67
|
+
debug("Avatar deleted successfully")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# Resolve relative paths to absolute paths
|
|
71
|
+
if not os.path.isabs(avatar_path):
|
|
72
|
+
# Convert relative path to absolute path relative to current working directory
|
|
73
|
+
avatar_path = os.path.abspath(avatar_path)
|
|
74
|
+
debug(f"Resolved relative path to absolute path: {avatar_path}")
|
|
75
|
+
|
|
76
|
+
# Want to set avatar from file
|
|
77
|
+
debug(f"Setting group avatar from file: {avatar_path}")
|
|
78
|
+
try:
|
|
79
|
+
with open(avatar_path, "rb") as avatar_file:
|
|
80
|
+
gitlab_group.avatar = avatar_file
|
|
81
|
+
gitlab_group.save()
|
|
82
|
+
debug("Group avatar uploaded successfully")
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
error_msg = f"Group avatar file not found: {avatar_path}"
|
|
85
|
+
debug(error_msg)
|
|
86
|
+
raise FileNotFoundError(error_msg)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
error_msg = f"Error uploading group avatar: {str(e)}"
|
|
89
|
+
debug(error_msg)
|
|
90
|
+
raise Exception(error_msg) from e
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from logging import info
|
|
3
|
+
from gitlabform.gitlab import GitLab
|
|
4
|
+
from gitlab.v4.objects import Group
|
|
5
|
+
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
from gitlabform.processors.util.variables_processor import VariablesProcessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroupVariablesProcessor(AbstractProcessor):
|
|
11
|
+
def __init__(self, gitlab: GitLab):
|
|
12
|
+
super().__init__("group_variables", gitlab)
|
|
13
|
+
self._variables_processor = VariablesProcessor(self._needs_update)
|
|
14
|
+
|
|
15
|
+
def _process_configuration(self, project_and_group: str, configuration: Dict[str, Any]) -> None:
|
|
16
|
+
group: Group = self.gl.get_group_by_path_cached(project_and_group)
|
|
17
|
+
|
|
18
|
+
configured_variables = configuration.get("group_variables", {})
|
|
19
|
+
enforce_mode: bool = configured_variables.get("enforce", False)
|
|
20
|
+
|
|
21
|
+
if enforce_mode:
|
|
22
|
+
info(f"Enforce mode enabled for variables in {project_and_group}")
|
|
23
|
+
# Remove 'enforce' key from the config so that it's not treated as a variable
|
|
24
|
+
configured_variables.pop("enforce")
|
|
25
|
+
|
|
26
|
+
self._variables_processor.process_variables(group, configured_variables, enforce_mode)
|