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.
Files changed (79) hide show
  1. gitlabform/__init__.py +719 -0
  2. gitlabform/configuration/__init__.py +12 -0
  3. gitlabform/configuration/common.py +19 -0
  4. gitlabform/configuration/core.py +323 -0
  5. gitlabform/configuration/groups.py +127 -0
  6. gitlabform/configuration/projects.py +73 -0
  7. gitlabform/configuration/transform.py +259 -0
  8. gitlabform/constants.py +7 -0
  9. gitlabform/gitlab/__init__.py +108 -0
  10. gitlabform/gitlab/commits.py +39 -0
  11. gitlabform/gitlab/core.py +334 -0
  12. gitlabform/gitlab/group_badges.py +50 -0
  13. gitlabform/gitlab/group_ldap_links.py +40 -0
  14. gitlabform/gitlab/groups.py +96 -0
  15. gitlabform/gitlab/merge_requests.py +57 -0
  16. gitlabform/gitlab/pipelines.py +23 -0
  17. gitlabform/gitlab/project_badges.py +52 -0
  18. gitlabform/gitlab/project_deploy_keys.py +102 -0
  19. gitlabform/gitlab/project_merge_requests_approvals.py +94 -0
  20. gitlabform/gitlab/project_protected_environments.py +37 -0
  21. gitlabform/gitlab/projects.py +151 -0
  22. gitlabform/gitlab/python_gitlab.py +251 -0
  23. gitlabform/gitlab/variables.py +47 -0
  24. gitlabform/lists/__init__.py +62 -0
  25. gitlabform/lists/filter.py +99 -0
  26. gitlabform/lists/groups.py +87 -0
  27. gitlabform/lists/projects.py +239 -0
  28. gitlabform/output.py +46 -0
  29. gitlabform/processors/__init__.py +43 -0
  30. gitlabform/processors/abstract_processor.py +187 -0
  31. gitlabform/processors/application/__init__.py +17 -0
  32. gitlabform/processors/application/application_settings_processor.py +39 -0
  33. gitlabform/processors/defining_keys.py +152 -0
  34. gitlabform/processors/group/__init__.py +48 -0
  35. gitlabform/processors/group/group_badges_processor.py +17 -0
  36. gitlabform/processors/group/group_hooks_processor.py +75 -0
  37. gitlabform/processors/group/group_labels_processor.py +28 -0
  38. gitlabform/processors/group/group_ldap_links_processor.py +16 -0
  39. gitlabform/processors/group/group_members_processor.py +287 -0
  40. gitlabform/processors/group/group_push_rules_processor.py +44 -0
  41. gitlabform/processors/group/group_saml_links_processor.py +65 -0
  42. gitlabform/processors/group/group_settings_processor.py +90 -0
  43. gitlabform/processors/group/group_variables_processor.py +26 -0
  44. gitlabform/processors/multiple_entities_processor.py +171 -0
  45. gitlabform/processors/project/__init__.py +80 -0
  46. gitlabform/processors/project/badges_processor.py +17 -0
  47. gitlabform/processors/project/branches_processor.py +514 -0
  48. gitlabform/processors/project/deploy_keys_processor.py +18 -0
  49. gitlabform/processors/project/files_processor.py +301 -0
  50. gitlabform/processors/project/hooks_processor.py +64 -0
  51. gitlabform/processors/project/integrations_processor.py +33 -0
  52. gitlabform/processors/project/job_token_scope_processor.py +216 -0
  53. gitlabform/processors/project/members_processor.py +204 -0
  54. gitlabform/processors/project/merge_requests_approval_rules.py +17 -0
  55. gitlabform/processors/project/merge_requests_approvals.py +59 -0
  56. gitlabform/processors/project/project_labels_processor.py +27 -0
  57. gitlabform/processors/project/project_processor.py +62 -0
  58. gitlabform/processors/project/project_push_rules_processor.py +52 -0
  59. gitlabform/processors/project/project_security_settings.py +66 -0
  60. gitlabform/processors/project/project_settings_processor.py +239 -0
  61. gitlabform/processors/project/project_variables_processor.py +94 -0
  62. gitlabform/processors/project/remote_mirrors_processor.py +278 -0
  63. gitlabform/processors/project/resource_groups_processor.py +48 -0
  64. gitlabform/processors/project/schedules_processor.py +208 -0
  65. gitlabform/processors/project/tags_processor.py +108 -0
  66. gitlabform/processors/shared/__init__.py +0 -0
  67. gitlabform/processors/shared/protected_environments_processor.py +20 -0
  68. gitlabform/processors/util/__init__.py +0 -0
  69. gitlabform/processors/util/decorators.py +44 -0
  70. gitlabform/processors/util/difference_logger.py +70 -0
  71. gitlabform/processors/util/labels_processor.py +120 -0
  72. gitlabform/processors/util/variables_processor.py +143 -0
  73. gitlabform/run.py +9 -0
  74. gitlabform/util.py +7 -0
  75. gitlabform-0.0.540a0.dist-info/METADATA +54 -0
  76. gitlabform-0.0.540a0.dist-info/RECORD +79 -0
  77. gitlabform-0.0.540a0.dist-info/WHEEL +4 -0
  78. gitlabform-0.0.540a0.dist-info/entry_points.txt +9 -0
  79. 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)