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,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
|