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,204 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import critical, info, warning, error
|
|
3
|
+
from gitlab import GitlabDeleteError
|
|
4
|
+
from gitlab.v4.objects import Project, User
|
|
5
|
+
|
|
6
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
7
|
+
from gitlabform.gitlab import GitLab
|
|
8
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MembersProcessor(AbstractProcessor):
|
|
12
|
+
def __init__(self, gitlab: GitLab):
|
|
13
|
+
super().__init__("members", gitlab)
|
|
14
|
+
|
|
15
|
+
def _process_configuration(self, project_and_group: str, configuration: dict):
|
|
16
|
+
keep_bots = configuration.get("members|keep_bots", False)
|
|
17
|
+
|
|
18
|
+
enforce_members = configuration.get("members|enforce", False)
|
|
19
|
+
|
|
20
|
+
groups = configuration.get("members|groups", {})
|
|
21
|
+
|
|
22
|
+
users = configuration.get("members|users", {})
|
|
23
|
+
|
|
24
|
+
if not groups and not users and not enforce_members:
|
|
25
|
+
critical(
|
|
26
|
+
"Project members configuration section has to contain"
|
|
27
|
+
" either 'users' or 'groups' non-empty keys"
|
|
28
|
+
" (unless you want to enforce no direct members)."
|
|
29
|
+
)
|
|
30
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
31
|
+
|
|
32
|
+
self._process_groups(project_and_group, groups, enforce_members)
|
|
33
|
+
self._process_users(project_and_group, users, enforce_members, keep_bots)
|
|
34
|
+
|
|
35
|
+
def _process_groups(self, project_and_group: str, groups: dict, enforce_members: bool):
|
|
36
|
+
if groups:
|
|
37
|
+
info("Processing groups as members...")
|
|
38
|
+
|
|
39
|
+
current_groups = self.gitlab.get_groups_from_project(project_and_group)
|
|
40
|
+
for group in groups:
|
|
41
|
+
info(f"Processing group {group}...")
|
|
42
|
+
expires_at = groups[group]["expires_at"].strftime("%Y-%m-%d") if "expires_at" in groups[group] else None
|
|
43
|
+
access_level = groups[group]["group_access"] if "group_access" in groups[group] else None
|
|
44
|
+
|
|
45
|
+
# we only add the group if it doesn't have the correct settings
|
|
46
|
+
common_group_name = group.lower()
|
|
47
|
+
if (
|
|
48
|
+
common_group_name in current_groups
|
|
49
|
+
and expires_at == current_groups[common_group_name]["expires_at"]
|
|
50
|
+
and access_level == current_groups[common_group_name]["group_access_level"]
|
|
51
|
+
):
|
|
52
|
+
info(f"Ignoring group '{common_group_name}' as it is already a member")
|
|
53
|
+
info(f"Current settings for '{common_group_name}' are: {current_groups[common_group_name]}")
|
|
54
|
+
else:
|
|
55
|
+
info(f"Setting group '{common_group_name}' as a member")
|
|
56
|
+
access = access_level
|
|
57
|
+
expiry = expires_at
|
|
58
|
+
|
|
59
|
+
# we will remove group access first and then re-add them,
|
|
60
|
+
# to ensure that the groups have the expected access level
|
|
61
|
+
self.gitlab.unshare_with_group(project_and_group, common_group_name)
|
|
62
|
+
self.gitlab.share_with_group(project_and_group, common_group_name, access, expiry)
|
|
63
|
+
|
|
64
|
+
if enforce_members:
|
|
65
|
+
current_groups = self.gitlab.get_groups_from_project(project_and_group)
|
|
66
|
+
|
|
67
|
+
groups_in_config = [group_name.lower() for group_name in groups.keys()]
|
|
68
|
+
groups_in_gitlab = current_groups.keys()
|
|
69
|
+
groups_not_in_config = set(groups_in_gitlab) - set(groups_in_config)
|
|
70
|
+
|
|
71
|
+
for group_not_in_config in groups_not_in_config:
|
|
72
|
+
info(f"Removing group '{group_not_in_config}' that is not configured to be a member.")
|
|
73
|
+
self.gitlab.unshare_with_group(project_and_group, group_not_in_config)
|
|
74
|
+
else:
|
|
75
|
+
info("Not enforcing group members.")
|
|
76
|
+
|
|
77
|
+
def _process_users(
|
|
78
|
+
self,
|
|
79
|
+
project_and_group: str,
|
|
80
|
+
users: dict,
|
|
81
|
+
enforce_members: bool,
|
|
82
|
+
keep_bots: bool,
|
|
83
|
+
):
|
|
84
|
+
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
85
|
+
|
|
86
|
+
current_members = self._get_members_from_project(project)
|
|
87
|
+
|
|
88
|
+
if users:
|
|
89
|
+
info("Processing users as members...")
|
|
90
|
+
|
|
91
|
+
for user in users:
|
|
92
|
+
info(f"Processing user '{user}'...")
|
|
93
|
+
|
|
94
|
+
gitlab_user = self.gl.get_user_by_username_cached(user)
|
|
95
|
+
if gitlab_user is None:
|
|
96
|
+
warning(f"Could not find user '{user}' in Gitlab, skipping...")
|
|
97
|
+
continue
|
|
98
|
+
user_id = gitlab_user.id
|
|
99
|
+
|
|
100
|
+
expires_at = users[user]["expires_at"].strftime("%Y-%m-%d") if "expires_at" in users[user] else None
|
|
101
|
+
access_level = users[user]["access_level"] if "access_level" in users[user] else None
|
|
102
|
+
member_role_id_or_name = users[user]["member_role"] if "member_role" in users[user] else None
|
|
103
|
+
if member_role_id_or_name:
|
|
104
|
+
group_name_and_path = project.namespace["full_path"]
|
|
105
|
+
member_role_id = self.gl.get_member_role_id_cached(member_role_id_or_name, group_name_and_path)
|
|
106
|
+
else:
|
|
107
|
+
member_role_id = None
|
|
108
|
+
|
|
109
|
+
# we only add the user if it doesn't have the correct settings.
|
|
110
|
+
# To make sure that the user hasn't been added in a different
|
|
111
|
+
# case, we enforce that the username is always in lowercase for
|
|
112
|
+
# checks.
|
|
113
|
+
common_username = user.lower()
|
|
114
|
+
|
|
115
|
+
if common_username in current_members:
|
|
116
|
+
current_member = current_members[common_username]
|
|
117
|
+
if hasattr(current_member, "member_role"):
|
|
118
|
+
member_role_id_before = current_member.member_role["id"]
|
|
119
|
+
else:
|
|
120
|
+
member_role_id_before = None
|
|
121
|
+
if (
|
|
122
|
+
expires_at == current_member.expires_at
|
|
123
|
+
and access_level == current_member.access_level
|
|
124
|
+
and member_role_id == member_role_id_before
|
|
125
|
+
):
|
|
126
|
+
info(f"Nothing to change for user '{common_username}' - same config now as to set.")
|
|
127
|
+
info(f"Current settings for '{common_username}' are: {current_members[common_username]}")
|
|
128
|
+
else:
|
|
129
|
+
info(
|
|
130
|
+
f"Editing user '{common_username}' membership to change their access level or expires at",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
project_member = project.members.get(id=user_id)
|
|
134
|
+
project_member.access_level = access_level
|
|
135
|
+
project_member.member_role_id = member_role_id_before
|
|
136
|
+
if expires_at:
|
|
137
|
+
project_member.expires_at = expires_at
|
|
138
|
+
project_member.save()
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
info(
|
|
142
|
+
f"Adding user '{common_username}' who previously was not a member.",
|
|
143
|
+
)
|
|
144
|
+
create_data = {
|
|
145
|
+
"user_id": user_id,
|
|
146
|
+
"access_level": access_level,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if expires_at:
|
|
150
|
+
create_data["expires_at"] = expires_at
|
|
151
|
+
|
|
152
|
+
if member_role_id:
|
|
153
|
+
create_data["member_role_id"] = member_role_id
|
|
154
|
+
|
|
155
|
+
project.members.create(data=create_data)
|
|
156
|
+
|
|
157
|
+
if enforce_members:
|
|
158
|
+
info("Enforcing Project members")
|
|
159
|
+
# Enforce that all usernames are lowercase for comparisons.
|
|
160
|
+
users_in_config = [username.lower() for username in users.keys()]
|
|
161
|
+
users_in_gitlab = current_members.keys()
|
|
162
|
+
users_not_in_config = set(users_in_gitlab) - set(users_in_config)
|
|
163
|
+
for user_not_in_config in users_not_in_config:
|
|
164
|
+
info(f"Removing user '{user_not_in_config}' that is not configured to be a member.")
|
|
165
|
+
gl_user: User | None = self.gl.get_user_by_username_cached(user_not_in_config)
|
|
166
|
+
|
|
167
|
+
if gl_user is None:
|
|
168
|
+
# User does not exist an instance level but is for whatever reason present on a Group/Project
|
|
169
|
+
# We should raise error into Logs but not prevent the rest of GitLabForm from executing
|
|
170
|
+
# This error is more likely to be prevalent in Dedicated instances; it is unlikely for a User to
|
|
171
|
+
# be completely deleted from gitlab.com
|
|
172
|
+
error(
|
|
173
|
+
f"Could not find User '{user_not_in_config}' on the Instance so can not remove User from "
|
|
174
|
+
f"Project '{project_and_group}'"
|
|
175
|
+
)
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
if keep_bots and gl_user.bot:
|
|
179
|
+
info(f"Will not remove bot user '{user_not_in_config}' as the 'keep_bots' option is true.")
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
project.members.delete(id=gl_user.id)
|
|
184
|
+
except GitlabDeleteError as delete_error:
|
|
185
|
+
error(f"Member '{user_not_in_config}' could not be deleted: {delete_error}")
|
|
186
|
+
raise delete_error
|
|
187
|
+
else:
|
|
188
|
+
info("Not enforcing user members.")
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _get_members_from_project(project):
|
|
192
|
+
# Only get direct members from Python Gitlab (matches previous implementation)
|
|
193
|
+
# https://python-gitlab.readthedocs.io/en/stable/gl_objects/projects.html#id14
|
|
194
|
+
project_members = project.members.list(iterator=True)
|
|
195
|
+
existing_user_names_lower = dict()
|
|
196
|
+
|
|
197
|
+
for member in project_members:
|
|
198
|
+
# To make sure that the user hasn't been added in a different
|
|
199
|
+
# case, we enforce that the username is always in lowercase for
|
|
200
|
+
# checks.
|
|
201
|
+
existing_user_names_lower[member.username.lower()] = member
|
|
202
|
+
|
|
203
|
+
info(f"Existing project members {existing_user_names_lower}")
|
|
204
|
+
return existing_user_names_lower
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from gitlabform.gitlab import GitLab
|
|
2
|
+
from gitlabform.processors.defining_keys import Key, And
|
|
3
|
+
from gitlabform.processors.multiple_entities_processor import MultipleEntitiesProcessor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MergeRequestsApprovalRules(MultipleEntitiesProcessor):
|
|
7
|
+
def __init__(self, gitlab: GitLab):
|
|
8
|
+
super().__init__(
|
|
9
|
+
"merge_requests_approval_rules",
|
|
10
|
+
gitlab,
|
|
11
|
+
list_method_name="get_approval_rules",
|
|
12
|
+
add_method_name="add_approval_rule",
|
|
13
|
+
edit_method_name="edit_approval_rule",
|
|
14
|
+
delete_method_name="delete_approval_rule",
|
|
15
|
+
defining=Key("name"),
|
|
16
|
+
required_to_create_or_update=And(Key("name"), Key("approvals_required")),
|
|
17
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import debug, info, critical
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from gitlab.v4.objects import Project
|
|
5
|
+
|
|
6
|
+
from gitlabform import EXIT_INVALID_INPUT
|
|
7
|
+
from gitlabform.gitlab import GitLab
|
|
8
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
9
|
+
from gitlabform.processors.util.difference_logger import DifferenceLogger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MergeRequestsApprovals(AbstractProcessor):
|
|
13
|
+
def __init__(self, gitlab: GitLab):
|
|
14
|
+
super().__init__("merge_requests_approvals", gitlab)
|
|
15
|
+
self.get_entity_in_gitlab: Callable = getattr(self, "get_project_mr_approvals_settings")
|
|
16
|
+
|
|
17
|
+
def _process_configuration(self, project_path: str, configuration: dict) -> None:
|
|
18
|
+
info("Processing project merge requests approvals settings...")
|
|
19
|
+
project: Project = self.gl.get_project_by_path_cached(project_path)
|
|
20
|
+
|
|
21
|
+
mr_approval_settings_in_config: dict = configuration.get("merge_requests_approvals", {})
|
|
22
|
+
mr_approval_settings_in_gitlab: dict = project.approvals.get().asdict()
|
|
23
|
+
|
|
24
|
+
info(mr_approval_settings_in_gitlab)
|
|
25
|
+
info("merge_requests_approvals BEFORE: ^^^")
|
|
26
|
+
|
|
27
|
+
if self._needs_update(mr_approval_settings_in_gitlab, mr_approval_settings_in_config):
|
|
28
|
+
debug("Updating project merge requests approvals settings")
|
|
29
|
+
project.approvals.update(**mr_approval_settings_in_config)
|
|
30
|
+
|
|
31
|
+
info(project.approvals.get().asdict())
|
|
32
|
+
info("merge_requests_approvals AFTER: ^^^")
|
|
33
|
+
else:
|
|
34
|
+
info("No update needed for project merge requests approvals settings")
|
|
35
|
+
|
|
36
|
+
def get_project_mr_approvals_settings(self, project_path: str):
|
|
37
|
+
return self.gl.get_project_by_path_cached(project_path).approvals.get().asdict()
|
|
38
|
+
|
|
39
|
+
def _can_proceed(self, project_or_group: str, configuration: dict):
|
|
40
|
+
if "approvals_before_merge" in configuration["merge_requests_approvals"]:
|
|
41
|
+
critical(
|
|
42
|
+
"Setting the 'approvals_before_merge' in the 'merge_requests_approvals' sections is not allowed "
|
|
43
|
+
"as it is not clear which rule does it apply to. "
|
|
44
|
+
"Please set it inside the specific approval rules in the 'merge_requests_approval_rules' section."
|
|
45
|
+
)
|
|
46
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
47
|
+
else:
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
# TODO: duplicated logic with project_settings_processor.py. Should be refactored - ideally in the AbstractProcessor
|
|
51
|
+
def _print_diff(self, project_or_project_and_group: str, entity_config, diff_only_changed: bool):
|
|
52
|
+
entity_in_gitlab = self.get_project_mr_approvals_settings(project_or_project_and_group)
|
|
53
|
+
|
|
54
|
+
DifferenceLogger.log_diff(
|
|
55
|
+
f"{self.configuration_name} changes",
|
|
56
|
+
entity_in_gitlab,
|
|
57
|
+
entity_config,
|
|
58
|
+
only_changed=diff_only_changed,
|
|
59
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from gitlabform.gitlab import GitLab
|
|
4
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
5
|
+
|
|
6
|
+
from gitlab.v4.objects import Project
|
|
7
|
+
|
|
8
|
+
from gitlabform.processors.util.labels_processor import LabelsProcessor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectLabelsProcessor(AbstractProcessor):
|
|
12
|
+
def __init__(self, gitlab: GitLab):
|
|
13
|
+
super().__init__("labels", gitlab)
|
|
14
|
+
self._labels_processor = LabelsProcessor()
|
|
15
|
+
|
|
16
|
+
def _process_configuration(self, project_and_group: str, configuration: Dict):
|
|
17
|
+
configured_labels = configuration.get("labels", {})
|
|
18
|
+
|
|
19
|
+
enforce = configuration.get("labels|enforce", False)
|
|
20
|
+
|
|
21
|
+
# Remove 'enforce' key from the config so that it's not treated as a "label"
|
|
22
|
+
if enforce:
|
|
23
|
+
configured_labels.pop("enforce")
|
|
24
|
+
|
|
25
|
+
project: Project = self.gl.get_project_by_path_cached(project_and_group)
|
|
26
|
+
|
|
27
|
+
self._labels_processor.process_labels(configured_labels, enforce, project, self._needs_update)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from logging import info, error
|
|
2
|
+
from gitlabform.gitlab import GitLab
|
|
3
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
4
|
+
from gitlab import GitlabGetError, GitlabTransferProjectError
|
|
5
|
+
from gitlab.v4.objects import Project
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectProcessor(AbstractProcessor):
|
|
9
|
+
def __init__(self, gitlab: GitLab):
|
|
10
|
+
super().__init__("project", gitlab)
|
|
11
|
+
|
|
12
|
+
def _process_configuration(self, project_and_group: str, configuration: dict):
|
|
13
|
+
project_path_with_namespace: str = project_and_group
|
|
14
|
+
|
|
15
|
+
if configuration["project"].get("transfer_from") is not None:
|
|
16
|
+
source_project_path_with_namespace = configuration["project"].get("transfer_from")
|
|
17
|
+
|
|
18
|
+
# Check if the project was already transfered (i.e. in previous run) or a project with same path already exists
|
|
19
|
+
try:
|
|
20
|
+
project_in_config: Project = self.gl.get_project_by_path_cached(project_path_with_namespace)
|
|
21
|
+
info(
|
|
22
|
+
f"Project already exists: '{project_in_config.path_with_namespace}'. Ignoring 'transfer_from' config..."
|
|
23
|
+
)
|
|
24
|
+
except GitlabGetError:
|
|
25
|
+
# Project doesn't exist at the destination. Let's process the transfer request
|
|
26
|
+
project_to_be_transferred: Project = self.gl.get_project_by_path_cached(
|
|
27
|
+
source_project_path_with_namespace
|
|
28
|
+
)
|
|
29
|
+
destination_project_path = project_and_group.split("/")[-1]
|
|
30
|
+
# Check if the project path needs to be updated; In Gitlab, path maybe different than name
|
|
31
|
+
if destination_project_path != project_to_be_transferred.path:
|
|
32
|
+
info(
|
|
33
|
+
f"Updating the source project path from '{project_to_be_transferred.path}' to '{destination_project_path}'"
|
|
34
|
+
)
|
|
35
|
+
self.gl.projects.update(project_to_be_transferred.id, {"path": destination_project_path})
|
|
36
|
+
|
|
37
|
+
# TODO: Catch GitlabTransferProjectError exception.
|
|
38
|
+
# See the next comment for details.
|
|
39
|
+
# try:
|
|
40
|
+
project_transfer_destination_group, _ = project_and_group.rsplit("/", 1)
|
|
41
|
+
info(f"Transferring project to '{project_transfer_destination_group}' group...")
|
|
42
|
+
project_to_be_transferred.transfer(project_transfer_destination_group)
|
|
43
|
+
# TODO: Catch GitlabTransferProjectError exception.
|
|
44
|
+
# The above code can run into exception for various reasons.
|
|
45
|
+
# We should catch this exception and log a custom error message with hints.
|
|
46
|
+
# In some scenarios we want to break and in some we want to proceed and attempt a transfer regardless
|
|
47
|
+
# For more details, see: https://github.com/gitlabform/gitlabform/issues/611
|
|
48
|
+
# except GitlabTransferProjectError as e:
|
|
49
|
+
# critical(
|
|
50
|
+
# "Encountered error transferring project. Please check if project transfer requirements were met. Docs: https://docs.gitlab.com/ee/user/project/settings/index.html#transfer-a-project-to-another-namespace"
|
|
51
|
+
# )
|
|
52
|
+
# raise
|
|
53
|
+
|
|
54
|
+
if configuration["project"].get("archive") is not None:
|
|
55
|
+
project: Project = self.gl.get_project_by_path_cached(project_path_with_namespace)
|
|
56
|
+
|
|
57
|
+
if configuration["project"].get("archive") is True:
|
|
58
|
+
info("Archiving project...")
|
|
59
|
+
project.archive()
|
|
60
|
+
elif configuration["project"].get("archive") is False:
|
|
61
|
+
info("Unarchiving project...")
|
|
62
|
+
project.unarchive()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from logging import debug
|
|
2
|
+
from gitlabform.gitlab import GitLab
|
|
3
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
4
|
+
from gitlab.v4.objects.projects import Project
|
|
5
|
+
from gitlab.exceptions import GitlabGetError, GitlabParsingError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectPushRulesProcessor(AbstractProcessor):
|
|
9
|
+
def __init__(self, gitlab: GitLab):
|
|
10
|
+
super().__init__("project_push_rules", gitlab)
|
|
11
|
+
|
|
12
|
+
def _process_configuration(self, project_path: str, configuration: dict):
|
|
13
|
+
configured_project_push_rules = configuration.get("project_push_rules", {})
|
|
14
|
+
project: Project = self.gl.get_project_by_path_cached(project_path)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
existing_push_rules = project.pushrules.get()
|
|
18
|
+
except GitlabGetError as e:
|
|
19
|
+
if e.response_code == 404:
|
|
20
|
+
debug(f"No existing push rules for '{project.name}', creating new push rules.")
|
|
21
|
+
self.create_project_push_rules(project, configured_project_push_rules)
|
|
22
|
+
return
|
|
23
|
+
except GitlabParsingError as e:
|
|
24
|
+
# Known issue from GitLab API when project push rule has never been configured.
|
|
25
|
+
# In that scenario, the API returns `null` instead of an appropriate message
|
|
26
|
+
# and error code.
|
|
27
|
+
# See GitLab issue : https://gitlab.com/gitlab-org/gitlab/-/issues/513331
|
|
28
|
+
# Until the issue is resolved, we will assume push rule is not configured.
|
|
29
|
+
# When the above issue is resolved, this exception block can be removed.
|
|
30
|
+
|
|
31
|
+
debug(
|
|
32
|
+
f"Cannot determine if push rule is currently configured for '{project.name}', attempting to create new push rules."
|
|
33
|
+
)
|
|
34
|
+
self.create_project_push_rules(project, configured_project_push_rules)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if self._needs_update(existing_push_rules.asdict(), configured_project_push_rules):
|
|
38
|
+
self.update_push_rules(existing_push_rules, configured_project_push_rules)
|
|
39
|
+
else:
|
|
40
|
+
debug("No update needed for Project Push Rules")
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def update_push_rules(push_rules, configured_project_push_rules: dict):
|
|
44
|
+
for key, value in configured_project_push_rules.items():
|
|
45
|
+
debug(f"Updating setting {key} to value {value}")
|
|
46
|
+
setattr(push_rules, key, value)
|
|
47
|
+
push_rules.save()
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def create_project_push_rules(project, push_rules_config: dict):
|
|
51
|
+
debug(f"Creating push rules with configuration: {push_rules_config}")
|
|
52
|
+
project.pushrules.create(push_rules_config)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from logging import debug, warning
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
from gitlab.v4.objects import Project
|
|
4
|
+
|
|
5
|
+
from gitlabform.gitlab import GitLab
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
from gitlabform.processors.util.difference_logger import DifferenceLogger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectSecuritySettingsProcessor(AbstractProcessor):
|
|
11
|
+
def __init__(self, gitlab: GitLab):
|
|
12
|
+
super().__init__("project_security_settings", gitlab)
|
|
13
|
+
|
|
14
|
+
def _process_configuration(self, project_name: str, configuration: dict) -> None:
|
|
15
|
+
project: Project = self.gl.get_project_by_path_cached(project_name)
|
|
16
|
+
|
|
17
|
+
security_settings_in_config = configuration.get("project_security_settings", {})
|
|
18
|
+
security_settings_in_gitlab = self.get_project_security_settings(project)
|
|
19
|
+
|
|
20
|
+
if self._needs_update(security_settings_in_gitlab, security_settings_in_config):
|
|
21
|
+
debug("Updating project security settings")
|
|
22
|
+
self._update_project_security_settings(project, security_settings_in_gitlab, security_settings_in_config)
|
|
23
|
+
else:
|
|
24
|
+
debug("No update needed for project security settings")
|
|
25
|
+
|
|
26
|
+
def get_project_security_settings(self, project: Project) -> dict:
|
|
27
|
+
"""Retrieve project security settings using python-gitlab."""
|
|
28
|
+
try:
|
|
29
|
+
# TODO: python-gitlab does not yet support retrieving project security settings
|
|
30
|
+
# via its dedicated manager, so we use a lower-level http_get method here.
|
|
31
|
+
# Switch to native method once supported by python-gitlab.
|
|
32
|
+
|
|
33
|
+
path = f"/projects/{project.id}/security_settings"
|
|
34
|
+
result = self.gl.http_get(path)
|
|
35
|
+
# http_get can return Response for streamed requests, but we're not streaming
|
|
36
|
+
# so it will always be a dict
|
|
37
|
+
return cast(dict[str, Any], result)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
warning(f"Failed to get project security settings for project {project.path_with_namespace}: {e}")
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def _update_project_security_settings(self, project: Project, initial_settings: dict, settings: dict) -> None:
|
|
43
|
+
"""Update project security settings using python-gitlab."""
|
|
44
|
+
try:
|
|
45
|
+
# TODO: python-gitlab does not yet support updating project security settings
|
|
46
|
+
# via its dedicated manager, so we use a lower-level http_put method here.
|
|
47
|
+
# Switch to native method once supported by python-gitlab.
|
|
48
|
+
debug(initial_settings)
|
|
49
|
+
debug("project_security_settings BEFORE: ^^^")
|
|
50
|
+
path = f"/projects/{project.id}/security_settings"
|
|
51
|
+
updated_security_config = self.gl.http_put(path, post_data=settings)
|
|
52
|
+
debug(cast(dict[str, Any], updated_security_config))
|
|
53
|
+
debug("project_security_settings AFTER: ^^^")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
warning(f"Failed to update project security settings for project {project.path_with_namespace}: {e}")
|
|
56
|
+
|
|
57
|
+
def _print_diff(self, project_or_project_and_group: str, entity_config, diff_only_changed: bool):
|
|
58
|
+
project = self.gl.get_project_by_path_cached(project_or_project_and_group)
|
|
59
|
+
entity_in_gitlab = self.get_project_security_settings(project)
|
|
60
|
+
|
|
61
|
+
DifferenceLogger.log_diff(
|
|
62
|
+
f"{self.configuration_name} changes",
|
|
63
|
+
entity_in_gitlab,
|
|
64
|
+
entity_config,
|
|
65
|
+
only_changed=diff_only_changed,
|
|
66
|
+
)
|