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,301 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from logging import debug, info, warning, critical
7
+ from typing import List
8
+
9
+ from jinja2 import Environment, FileSystemLoader
10
+ from gitlab import GitlabGetError, GitlabUpdateError
11
+ from gitlab.v4.objects import Project, ProjectFile, ProjectBranch
12
+ from gitlab.base import RESTObject
13
+
14
+ from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
15
+ from gitlabform.configuration import Configuration
16
+ from gitlabform.gitlab import GitLab
17
+ from gitlabform.processors.abstract_processor import AbstractProcessor
18
+ from gitlabform.processors.project.branches_processor import BranchesProcessor
19
+
20
+
21
+ class FilesProcessor(AbstractProcessor):
22
+ def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
23
+ super().__init__("files", gitlab)
24
+ self.config = config
25
+ self.strict = strict
26
+ self.branch_processor = BranchesProcessor(gitlab, strict)
27
+
28
+ def _process_configuration(self, project_and_group: str, configuration: dict):
29
+ for file in sorted(configuration["files"]):
30
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
31
+ debug("Processing file '%s'...", file)
32
+
33
+ if configuration.get("files|" + file + "|skip"):
34
+ debug("Skipping file '%s'", file)
35
+ continue
36
+
37
+ config_target_ref = configuration["files"][file]["branches"]
38
+ branches_to_update: List[RESTObject] = []
39
+
40
+ if isinstance(config_target_ref, str):
41
+ # Target ref could be either 'all' or 'protected'.
42
+ # Get a list of branches that should be updated.
43
+ if config_target_ref == "all":
44
+ branches_to_update.extend(project.branches.list(get_all=True, lazy=True))
45
+ elif config_target_ref == "protected":
46
+ branches_to_update.extend(project.protectedbranches.list(get_all=True, lazy=True))
47
+ elif isinstance(config_target_ref, list):
48
+ # Get a list of branches from the config that should be updated.
49
+ for branch_name in config_target_ref:
50
+ try:
51
+ branches_to_update.append(project.branches.get(branch_name))
52
+ except GitlabGetError:
53
+ message = f"! Branch '{branch_name}' not found, not processing file '{file}' in it"
54
+ if self.strict:
55
+ critical(message)
56
+ sys.exit(EXIT_INVALID_INPUT)
57
+ else:
58
+ warning(message)
59
+
60
+ debug(
61
+ "File '%s' to be updated in '%s' branche(s)",
62
+ file,
63
+ len(branches_to_update),
64
+ )
65
+
66
+ for branch in branches_to_update:
67
+ info(f"Processing file '{file}' in branch '{branch.name}'")
68
+
69
+ if configuration.get("files|" + file + "|content") and configuration.get("files|" + file + "|file"):
70
+ critical(
71
+ f"File '{file}' in '{project_and_group}' has both `content` and `file` set - "
72
+ "use only one of these keys."
73
+ )
74
+ sys.exit(EXIT_INVALID_INPUT)
75
+
76
+ if configuration.get("files|" + file + "|delete"):
77
+ try:
78
+ file_to_delete: ProjectFile = project.files.get(file_path=file, ref=branch.name)
79
+ debug("Deleting file '%s' in branch '%s'", file, branch.name)
80
+ self.modify_file_dealing_with_branch_protection(
81
+ project,
82
+ branch,
83
+ file_to_delete,
84
+ "delete",
85
+ configuration,
86
+ )
87
+ except GitlabGetError:
88
+ debug(
89
+ "Not deleting file '%s' in branch '%s' (already doesn't exist)",
90
+ file,
91
+ branch.name,
92
+ )
93
+ else:
94
+ # change or create file
95
+
96
+ if configuration.get("files|" + file + "|content"):
97
+ new_content = configuration.get("files|" + file + "|content")
98
+ else:
99
+ path_in_config = Path(str(configuration.get("files|" + file + "|file")))
100
+ if path_in_config.is_absolute():
101
+ effective_path = path_in_config
102
+ else:
103
+ # relative paths are relative to config file location
104
+ effective_path = Path(os.path.join(self.config.config_dir, str(path_in_config)))
105
+ new_content = effective_path.read_text()
106
+
107
+ # templating is documented to be enabled by default,
108
+ # see https://gitlabform.github.io/gitlabform/reference/files/#files
109
+ templating_enabled = True
110
+
111
+ if configuration.get("files|" + file + "|template", templating_enabled):
112
+ new_content = self.get_file_content_as_template(
113
+ new_content,
114
+ project_and_group,
115
+ **configuration.get("files|" + file + "|jinja_env", dict()),
116
+ )
117
+
118
+ try:
119
+ # Returns base64 encoded content: https://python-gitlab.readthedocs.io/en/stable/gl_objects/projects.html#project-files
120
+ repo_file: ProjectFile = project.files.get(file_path=file, ref=branch.name)
121
+ decoded_file: bytes = repo_file.decode()
122
+ current_content: str = decoded_file.decode("utf-8")
123
+
124
+ if current_content != new_content:
125
+ if configuration.get("files|" + file + "|overwrite"):
126
+ debug(
127
+ "Changing file '%s' in branch '%s'",
128
+ file,
129
+ branch.name,
130
+ )
131
+ self.modify_file_dealing_with_branch_protection(
132
+ project,
133
+ branch,
134
+ repo_file,
135
+ "modify",
136
+ configuration,
137
+ new_content,
138
+ )
139
+ else:
140
+ debug(
141
+ "Not changing file '%s' in branch '%s' - overwrite flag not set.",
142
+ file,
143
+ branch.name,
144
+ )
145
+ else:
146
+ debug(
147
+ "Not changing file '%s' in branch '%s' - it's content is already" " as provided)",
148
+ file,
149
+ branch.name,
150
+ )
151
+ except GitlabGetError:
152
+ debug("Creating file '%s' in branch '%s'", file, branch.name)
153
+ self.modify_file_dealing_with_branch_protection(
154
+ project,
155
+ branch,
156
+ file,
157
+ "add",
158
+ configuration,
159
+ new_content,
160
+ )
161
+
162
+ if configuration.get("files|" + file + "|only_first_branch"):
163
+ info("Skipping other branches for this file, as configured.")
164
+ break
165
+
166
+ def modify_file_dealing_with_branch_protection(
167
+ self,
168
+ project: Project,
169
+ branch: RESTObject,
170
+ file_to_operate_on: str | ProjectFile,
171
+ operation: str,
172
+ configuration: dict,
173
+ new_content=None,
174
+ ):
175
+ # perhaps your user permissions are ok to just perform this operation regardless
176
+ # of the branch protection...
177
+
178
+ try:
179
+ self.just_modify_file(
180
+ project,
181
+ branch,
182
+ file_to_operate_on,
183
+ operation,
184
+ configuration,
185
+ new_content,
186
+ )
187
+
188
+ except GitlabUpdateError as e:
189
+ if (
190
+ e.response_code == 400 or e.response_code == 403
191
+ ) and "You are not allowed to push into this branch" in e.error_message:
192
+ # If the project is archived, modifying files is not allowed
193
+ if project.archived:
194
+ critical(f"Project is archived, cannot modify files in it.: {e.error_message}")
195
+ sys.exit(EXIT_PROCESSING_ERROR)
196
+
197
+ # Otherwise, unprotect the branch but only if we know how to protect it again
198
+ if configuration.get("branches|" + branch.name + "|protected"):
199
+ debug(f"> Temporarily unprotecting the branch to '{operation}' a file in it...")
200
+ # Delete operation on protected branch removes the protection only
201
+ project.protectedbranches.delete(branch.name)
202
+ else:
203
+ critical(
204
+ f"Operation '{operation}' on file in branch {branch.name} not permitted."
205
+ f" We don't have a branch protection configuration provided for this"
206
+ f" branch. Breaking as we cannot unprotect the branch as we would not know"
207
+ f" how to protect it again."
208
+ )
209
+ sys.exit(EXIT_INVALID_INPUT)
210
+
211
+ try:
212
+ debug("> Attempt updating file again")
213
+ self.just_modify_file(
214
+ project,
215
+ branch,
216
+ file_to_operate_on,
217
+ operation,
218
+ configuration,
219
+ new_content,
220
+ )
221
+
222
+ finally:
223
+ # ...and protect the branch again after the operation
224
+ if configuration.get("branches|" + branch.name + "|protected"):
225
+ debug("> Protecting the branch again.")
226
+ branch_config: dict = configuration["branches"][branch.name]
227
+ self.branch_processor.protect_branch(project, branch.name, branch_config)
228
+
229
+ else:
230
+ raise e
231
+
232
+ def just_modify_file(
233
+ self,
234
+ project: Project,
235
+ branch: RESTObject,
236
+ file_to_operate_on: str | ProjectFile,
237
+ operation: str,
238
+ configuration: dict,
239
+ new_content=None,
240
+ ):
241
+ if operation == "modify" and isinstance(file_to_operate_on, ProjectFile):
242
+ file_to_operate_on.content = new_content
243
+ file_to_operate_on.save(
244
+ commit_message=self.get_commit_message_for_file_change(
245
+ "change", file_to_operate_on.file_path, configuration
246
+ ),
247
+ branch=branch.name,
248
+ )
249
+ elif operation == "add" and isinstance(file_to_operate_on, str):
250
+ project.files.create(
251
+ {
252
+ "file_path": file_to_operate_on,
253
+ "branch": branch.name,
254
+ "content": new_content,
255
+ "commit_message": self.get_commit_message_for_file_change(
256
+ "delete", file_to_operate_on, configuration
257
+ ),
258
+ }
259
+ )
260
+ elif operation == "delete" and isinstance(file_to_operate_on, ProjectFile):
261
+ file_to_operate_on.delete(
262
+ commit_message=self.get_commit_message_for_file_change(
263
+ "delete", file_to_operate_on.file_path, configuration
264
+ ),
265
+ branch=branch.name,
266
+ )
267
+
268
+ def get_file_content_as_template(self, template, project_and_group, **kwargs):
269
+ # Use jinja with variables project and group
270
+ rtemplate = Environment(
271
+ loader=FileSystemLoader("."),
272
+ autoescape=True,
273
+ keep_trailing_newline=True,
274
+ ).from_string(template)
275
+ return rtemplate.render(
276
+ project=self.get_project(project_and_group),
277
+ group=self.get_group(project_and_group),
278
+ **kwargs,
279
+ )
280
+
281
+ @staticmethod
282
+ def get_commit_message_for_file_change(operation, file, configuration: dict):
283
+ commit_message = configuration.get(
284
+ "files|" + file + "|commit_message",
285
+ "Automated %s made by gitlabform" % operation,
286
+ )
287
+
288
+ # add '[skip ci]' to commit message to skip CI job, as documented at
289
+ # https://docs.gitlab.com/ee/ci/yaml/README.html#skipping-jobs
290
+ skip_build = configuration.get("files|" + file + "|skip_ci")
291
+ skip_build_str = " [skip ci]" if skip_build else ""
292
+
293
+ return f"{commit_message}{skip_build_str}"
294
+
295
+ @staticmethod
296
+ def get_group(project_and_group):
297
+ return re.match("(.*)/.*", project_and_group).group(1)
298
+
299
+ @staticmethod
300
+ def get_project(project_and_group):
301
+ return re.match(".*/(.*)", project_and_group).group(1)
@@ -0,0 +1,64 @@
1
+ from logging import debug
2
+ from typing import Dict, Any, List
3
+
4
+ from gitlab.base import RESTObject, RESTObjectList
5
+ from gitlab.v4.objects import Project
6
+ from gitlab.v4.objects import ProjectHook
7
+
8
+ from gitlabform.gitlab import GitLab
9
+ from gitlabform.processors.abstract_processor import AbstractProcessor
10
+
11
+
12
+ class HooksProcessor(AbstractProcessor):
13
+ def __init__(self, gitlab: GitLab):
14
+ super().__init__("hooks", gitlab)
15
+
16
+ def _process_configuration(self, project_and_group: str, configuration: dict):
17
+ debug("Processing hooks...")
18
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
19
+ project_hooks: list[ProjectHook] = project.hooks.list(get_all=True)
20
+
21
+ hooks_in_config: tuple[str, ...] = tuple(x for x in sorted(configuration["hooks"]) if x != "enforce")
22
+
23
+ for hook in hooks_in_config:
24
+ hook_in_gitlab: RESTObject | None = next((h for h in project_hooks if h.url == hook), None)
25
+ hook_config = {"url": hook}
26
+ hook_config.update(configuration["hooks"][hook])
27
+
28
+ hook_id = hook_in_gitlab.id if hook_in_gitlab else None
29
+
30
+ # Process hooks configured for deletion
31
+ if configuration.get("hooks|" + hook + "|delete"):
32
+ if hook_id:
33
+ debug(f"Deleting hook '{hook}'")
34
+ project.hooks.delete(hook_id)
35
+ debug(f"Deleted hook '{hook}'")
36
+ else:
37
+ debug(f"Not deleting hook '{hook}', because it doesn't exist")
38
+ continue
39
+
40
+ # Process new hook creation
41
+ if not hook_id:
42
+ debug(f"Creating hook '{hook}'")
43
+ created_hook: RESTObject = project.hooks.create(hook_config)
44
+ debug(f"Created hook: {created_hook}")
45
+ continue
46
+
47
+ # Processing existing hook updates
48
+ gl_hook: dict = hook_in_gitlab.asdict() if hook_in_gitlab else {}
49
+ if self._needs_update(gl_hook, hook_config):
50
+ debug(f"The hook '{hook}' config is different from what's in gitlab OR it contains a token")
51
+ debug(f"Updating hook '{hook}'")
52
+ updated_hook: Dict[str, Any] = project.hooks.update(hook_id, hook_config)
53
+ debug(f"Updated hook: {updated_hook}")
54
+ else:
55
+ debug(f"Hook '{hook}' remains unchanged")
56
+
57
+ # Process hook config enforcements
58
+ if configuration.get("hooks|enforce"):
59
+ for gh in project_hooks:
60
+ if gh.url not in hooks_in_config:
61
+ debug(
62
+ f"Deleting hook '{gh.url}' currently setup in the project but it is not in the configuration and enforce is enabled"
63
+ )
64
+ project.hooks.delete(gh.id)
@@ -0,0 +1,33 @@
1
+ from logging import info
2
+
3
+ from gitlab.exceptions import GitlabDeleteError
4
+ from gitlab.v4.objects import Project, ProjectIntegration
5
+ from gitlabform.gitlab import GitLab
6
+ from gitlabform.processors.abstract_processor import AbstractProcessor
7
+
8
+
9
+ class IntegrationsProcessor(AbstractProcessor):
10
+ def __init__(self, gitlab: GitLab):
11
+ super().__init__("integrations", gitlab)
12
+
13
+ def _process_configuration(self, project_and_group: str, configuration: dict):
14
+ configured_integrations = configuration.get("integrations", {})
15
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
16
+
17
+ for integration in sorted(configured_integrations):
18
+ gl_integration: ProjectIntegration = project.integrations.get(integration, lazy=True)
19
+
20
+ if configured_integrations[integration].get("delete"):
21
+ info(f"Deleting integration: {integration}")
22
+ try:
23
+ gl_integration.delete()
24
+ except GitlabDeleteError as e:
25
+ # If we get a 404 the integration does not exist, so we can ignore the error
26
+ if e.response_code == 404:
27
+ info(f"Integration {integration} does not exist, skipping deletion.")
28
+ else:
29
+ info(f"Failed to delete integration {integration}: {e}")
30
+ raise
31
+ else:
32
+ info(f"Setting integration: {integration}")
33
+ project.integrations.update(integration, configured_integrations[integration])
@@ -0,0 +1,216 @@
1
+ from typing import List
2
+
3
+ from gitlabform.gitlab import GitLab
4
+ from gitlabform.processors import AbstractProcessor
5
+ from logging import warning, info, debug
6
+
7
+ from gitlab.v4.objects import Project, ProjectJobTokenScope
8
+
9
+
10
+ class JobTokenScopeProcessor(AbstractProcessor):
11
+ def __init__(self, gitlab: GitLab):
12
+ super().__init__("job_token_scope", gitlab)
13
+
14
+ def _process_configuration(self, project_and_group: str, configuration: dict):
15
+ job_token_config = configuration.get("job_token_scope", {})
16
+ debug(f"Job Token Scope config: {job_token_config}")
17
+
18
+ project = self.gl.get_project_by_path_cached(project_and_group)
19
+ job_token_scope = project.job_token_scope.get()
20
+
21
+ limit_access_state_updated = self._process_limit_access_to_this_project_setting(
22
+ job_token_config, job_token_scope
23
+ )
24
+
25
+ if limit_access_state_updated:
26
+ # Refresh has no return but produces same result as project.job_token_scope.get()
27
+ # -> refreshes job_token_scope state with latest changes
28
+ job_token_scope.refresh()
29
+
30
+ allowlist_config = job_token_config.get("allowlist", {})
31
+ debug(f"configuration allowlist: {allowlist_config}")
32
+
33
+ info("Processing Job Token allowlist")
34
+
35
+ enforce = allowlist_config.get("enforce", False)
36
+
37
+ self._process_groups(job_token_scope, allowlist_config.get("groups", []), enforce)
38
+
39
+ self._process_projects(project, job_token_scope, allowlist_config.get("projects", []), enforce)
40
+
41
+ @staticmethod
42
+ def _process_limit_access_to_this_project_setting(configuration: dict, job_token_scope: ProjectJobTokenScope):
43
+ limit_access_to_this_project: bool = configuration.get("limit_access_to_this_project", True)
44
+
45
+ if limit_access_to_this_project != job_token_scope.inbound_enabled:
46
+ info(f"Updating project job token scope to limit access: {limit_access_to_this_project}")
47
+ job_token_scope.enabled = limit_access_to_this_project
48
+ job_token_scope.save()
49
+ return True
50
+ else:
51
+ info(f"Job Token Scope does not need updating")
52
+ return False
53
+
54
+ def _process_projects(
55
+ self,
56
+ project: Project,
57
+ job_token_scope: ProjectJobTokenScope,
58
+ projects_allowlist: List,
59
+ enforce: bool,
60
+ ):
61
+ if not projects_allowlist and enforce:
62
+ warning("Process will remove existing projects from allowlist, as none set in configuration")
63
+
64
+ existing_allowlist = job_token_scope.allowlist.list(get_all=True)
65
+
66
+ project_ids_to_allow = self._get_target_project_ids_from_config(projects_allowlist)
67
+
68
+ allowlist_updated = False
69
+ if len(project_ids_to_allow) > 0:
70
+ allowlist_updated = self._add_projects_to_allowlist(
71
+ project, job_token_scope, existing_allowlist, project_ids_to_allow
72
+ )
73
+
74
+ if enforce:
75
+ if allowlist_updated:
76
+ # Refresh has no return but produces same result as project.job_token_scope.get()
77
+ # -> refreshes job_token_scope state with latest changes
78
+ job_token_scope.refresh()
79
+
80
+ info("Enforce enabled, removing projects no longer defined in config from allowlist")
81
+ self._remove_projects_from_allowlist(project, job_token_scope, existing_allowlist, project_ids_to_allow)
82
+
83
+ def _process_groups(
84
+ self,
85
+ job_token_scope: ProjectJobTokenScope,
86
+ groups_allowlist: List,
87
+ enforce: bool,
88
+ ):
89
+ if not groups_allowlist and enforce:
90
+ warning("Process will remove existing groups from allowlist, as none set in configuration")
91
+
92
+ existing_allowlist = job_token_scope.groups_allowlist.list(get_all=True)
93
+
94
+ group_ids_to_allow = self._get_target_group_ids_from_config(groups_allowlist)
95
+
96
+ allowlist_updated = False
97
+ if len(group_ids_to_allow) > 0:
98
+ allowlist_updated = self._add_groups_to_allowlist(job_token_scope, existing_allowlist, group_ids_to_allow)
99
+
100
+ if enforce:
101
+ if allowlist_updated:
102
+ # Refresh has no return but produces same result as project.job_token_scope.get()
103
+ # -> refreshes job_token_scope state with latest changes
104
+ job_token_scope.refresh()
105
+
106
+ info("Enforce enabled, removing groups no longer defined in config from allowlist")
107
+ self._remove_groups_from_allowlist(job_token_scope, existing_allowlist, group_ids_to_allow)
108
+
109
+ def _remove_groups_from_allowlist(
110
+ self,
111
+ job_token_scope: ProjectJobTokenScope,
112
+ existing_allowlist,
113
+ target_group_ids: List,
114
+ ):
115
+ allowlist_updated = False
116
+ group_ids_to_remove = self._get_ids_to_remove_from_allowlist(existing_allowlist, target_group_ids)
117
+ for group_id in group_ids_to_remove:
118
+ allowlist_updated = True
119
+ job_token_scope.groups_allowlist.delete(group_id)
120
+ info("Deleted group %s from allowlist", group_id)
121
+
122
+ if allowlist_updated:
123
+ debug("Saving removed Groups allowlist changes")
124
+ job_token_scope.save()
125
+
126
+ @staticmethod
127
+ def _add_groups_to_allowlist(job_token_scope, existing_allowlist, group_ids_listed_in_config):
128
+ allowlist_updated = False
129
+
130
+ for group_id in group_ids_listed_in_config:
131
+ if any(allowed.get_id() == group_id for allowed in existing_allowlist):
132
+ # If already in allowlist, do nothing
133
+ debug(f"{group_id} already in Groups allowlist")
134
+ continue
135
+
136
+ allowlist_updated = True
137
+ job_token_scope.groups_allowlist.create({"target_group_id": group_id})
138
+ info(f"Added Group {group_id} to allowlist")
139
+
140
+ # If we have added something new to the allowlist then save the scope otherwise save API calls
141
+ if allowlist_updated:
142
+ debug("Saving added Groups allowlist changes")
143
+ job_token_scope.save()
144
+ return True
145
+ else:
146
+ return False
147
+
148
+ @staticmethod
149
+ def _add_projects_to_allowlist(project, job_token_scope, existing_allowlist, project_ids_listed_in_config):
150
+ allowlist_state_updated = False
151
+
152
+ for project_id in project_ids_listed_in_config:
153
+ if project_id != project.id:
154
+ if any(allowed.get_id() == project_id for allowed in existing_allowlist):
155
+ # If already in allowlist, do nothing
156
+ debug(f"{project_id} already in Projects allowlist")
157
+ continue
158
+
159
+ allowlist_state_updated = True
160
+ job_token_scope.allowlist.create({"target_project_id": project_id})
161
+ info(f"Added Project {project_id} to allowlist")
162
+
163
+ # If we have added something new to the allowlist then save the scope otherwise save API calls
164
+ if allowlist_state_updated:
165
+ debug("Saving added Projects allowlist changes")
166
+ job_token_scope.save()
167
+ return True
168
+ else:
169
+ return False
170
+
171
+ def _remove_projects_from_allowlist(
172
+ self,
173
+ project: Project,
174
+ job_token_scope: ProjectJobTokenScope,
175
+ existing_allowlist,
176
+ target_project_ids: List,
177
+ ):
178
+ removed_items_from_allowlist = False
179
+ project_ids_to_remove = self._get_ids_to_remove_from_allowlist(existing_allowlist, target_project_ids)
180
+ for project_id in project_ids_to_remove:
181
+ if project_id != project.id:
182
+ removed_items_from_allowlist = True
183
+ job_token_scope.allowlist.delete(project_id)
184
+ info("Deleted project %s from allowlist", project_id)
185
+
186
+ if removed_items_from_allowlist:
187
+ debug("Saving removed Projects allowlist changes")
188
+ job_token_scope.save()
189
+
190
+ @staticmethod
191
+ def _get_ids_to_remove_from_allowlist(existing_allowlist, target_ids: List):
192
+ ids_to_remove = []
193
+
194
+ for allowed in existing_allowlist:
195
+ if allowed.id not in target_ids:
196
+ ids_to_remove.append(allowed.id)
197
+
198
+ return ids_to_remove
199
+
200
+ def _get_target_project_ids_from_config(self, projects_allowlist: List):
201
+ target_project_ids = []
202
+
203
+ for target_project_or_id in projects_allowlist:
204
+ target_project = self.gl.get_project_by_path_cached(target_project_or_id)
205
+ target_project_ids.append(target_project.id)
206
+
207
+ return target_project_ids
208
+
209
+ def _get_target_group_ids_from_config(self, groups_allowlist: List):
210
+ target_group_ids = []
211
+
212
+ for target_group_or_id in groups_allowlist:
213
+ target_group = self.gl.get_group_by_path_cached(target_group_or_id)
214
+ target_group_ids.append(target_group.id)
215
+
216
+ return target_group_ids