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,334 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from logging import debug, info, warning
|
|
5
|
+
from typing import Union
|
|
6
|
+
from urllib import parse
|
|
7
|
+
|
|
8
|
+
from packaging import version
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import version as package_version
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
# noinspection PyPackageRequirements
|
|
14
|
+
import urllib3
|
|
15
|
+
from requests.adapters import HTTPAdapter
|
|
16
|
+
|
|
17
|
+
# noinspection PyPackageRequirements
|
|
18
|
+
from urllib3.util.retry import Retry
|
|
19
|
+
|
|
20
|
+
from gitlabform.configuration import Configuration
|
|
21
|
+
from gitlabform.util import to_str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GitLabCore:
|
|
25
|
+
def __init__(self, config_path=None, config_string=None):
|
|
26
|
+
self.configuration = Configuration(config_path, config_string)
|
|
27
|
+
|
|
28
|
+
default_gitlab_config = {
|
|
29
|
+
"url": os.getenv("GITLAB_URL"),
|
|
30
|
+
"token": os.getenv("GITLAB_TOKEN"),
|
|
31
|
+
"ssl_verify": True,
|
|
32
|
+
"timeout": 10,
|
|
33
|
+
"max_retries": 3,
|
|
34
|
+
"backoff_factor": 0.25,
|
|
35
|
+
"retry_transient_errors": True,
|
|
36
|
+
}
|
|
37
|
+
gitlab_config_from_file = self.configuration.get("gitlab", {})
|
|
38
|
+
self.gitlab_config = {**default_gitlab_config, **gitlab_config_from_file}
|
|
39
|
+
|
|
40
|
+
self.session = requests.Session()
|
|
41
|
+
|
|
42
|
+
retries_status_forcelist = []
|
|
43
|
+
if self.gitlab_config["retry_transient_errors"]:
|
|
44
|
+
# 429 Too Many Requests is included to handle rate limiting
|
|
45
|
+
# Ideally we would like to handle Rate Limiting retrys based on 'Retry-After' header
|
|
46
|
+
# As done in python-gitlab: https://github.com/python-gitlab/python-gitlab/blob/main/docs/api-usage-advanced.rst#rate-limits
|
|
47
|
+
# 5xx status codes are included to retry after transient server errors
|
|
48
|
+
retries_status_forcelist = [429, 500, 502, 503, 504] + list(range(520, 531))
|
|
49
|
+
|
|
50
|
+
retries = Retry(
|
|
51
|
+
total=self.gitlab_config["max_retries"],
|
|
52
|
+
backoff_factor=self.gitlab_config["backoff_factor"],
|
|
53
|
+
status_forcelist=retries_status_forcelist,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
57
|
+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
58
|
+
|
|
59
|
+
self.session.verify = self.gitlab_config["ssl_verify"]
|
|
60
|
+
if not self.gitlab_config["ssl_verify"]:
|
|
61
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
62
|
+
|
|
63
|
+
self.gitlabform_version = package_version("gitlabform")
|
|
64
|
+
self.requests_version = package_version("requests")
|
|
65
|
+
self.session.headers.update(
|
|
66
|
+
{
|
|
67
|
+
"private-token": self.gitlab_config["token"],
|
|
68
|
+
"authorization": f"Bearer {self.gitlab_config['token']}",
|
|
69
|
+
"user-agent": f"GitLabForm/{self.gitlabform_version} (python-requests/{self.requests_version})",
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
version_response = self._make_requests_to_api("version")
|
|
75
|
+
info(
|
|
76
|
+
f"Connected to GitLab version: {version_response['version']} ({version_response['revision']}), Enterprise Edition: {version_response['enterprise']}"
|
|
77
|
+
)
|
|
78
|
+
self.version = version_response["version"]
|
|
79
|
+
self.enterprise = version_response["enterprise"]
|
|
80
|
+
|
|
81
|
+
if self.is_version_less_than("16"):
|
|
82
|
+
warning(
|
|
83
|
+
f"Support for GitLab version {self.version} is Deprecated. See Requirements: https://gitlabform.github.io/gitlabform/requirements/"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
current_user = self._make_requests_to_api("user")
|
|
87
|
+
if current_user.get("is_admin", False):
|
|
88
|
+
self.admin = True
|
|
89
|
+
else:
|
|
90
|
+
self.admin = False
|
|
91
|
+
info(f"Connected as: {current_user['username']}, admin: {'yes' if self.admin else 'no'}")
|
|
92
|
+
if not self.admin:
|
|
93
|
+
warning("Connected as non-admin. You may encounter permission issues.")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise TestRequestFailedException(e)
|
|
97
|
+
|
|
98
|
+
def get_configuration(self):
|
|
99
|
+
return self.configuration
|
|
100
|
+
|
|
101
|
+
def get_project(self, project_and_group_or_id):
|
|
102
|
+
return self._make_requests_to_api("projects/%s", project_and_group_or_id)
|
|
103
|
+
|
|
104
|
+
def is_version_at_least(self, min_version: Union[str, version.Version]) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Check if GitLab server version is at least the specified version
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
min_version: Version string like "15.4.0" or "16.0", or a Version object
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
bool: True if server version is >= min_version, False otherwise
|
|
113
|
+
"""
|
|
114
|
+
current_version = "0.0.0"
|
|
115
|
+
|
|
116
|
+
if self.version != "unknown":
|
|
117
|
+
sem_ver_regex = "^\d*\.\d*\.\d*"
|
|
118
|
+
# Get the pure semantic version from self.version, for example Gitlab will return 18.2.1-ee
|
|
119
|
+
match = re.search(sem_ver_regex, self.version)
|
|
120
|
+
if match:
|
|
121
|
+
current_version = match.group()
|
|
122
|
+
|
|
123
|
+
if isinstance(min_version, str):
|
|
124
|
+
min_version = version.parse(min_version)
|
|
125
|
+
|
|
126
|
+
return version.parse(current_version) >= min_version
|
|
127
|
+
|
|
128
|
+
def is_version_less_than(self, max_version: Union[str, version.Version]) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Check if GitLab server version is less than the specified version
|
|
131
|
+
Args:
|
|
132
|
+
max_version: Version string like "15.4.0" or "16.0", or a Version object
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if server version is < max_version, False otherwise
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
return not self.is_version_at_least(max_version)
|
|
139
|
+
|
|
140
|
+
@functools.lru_cache()
|
|
141
|
+
def _get_user_id(self, username: str) -> int:
|
|
142
|
+
users = self._make_requests_to_api("users?username=%s", username, "GET")
|
|
143
|
+
|
|
144
|
+
# this API endpoint is for lookup, not search, so 'username' has to be full and exact username
|
|
145
|
+
# also it's not possible to get more than 1 user as a result
|
|
146
|
+
|
|
147
|
+
if len(users) == 0:
|
|
148
|
+
raise NotFoundException("No users found when searching for username '%s'" % username)
|
|
149
|
+
|
|
150
|
+
return int(users[0]["id"])
|
|
151
|
+
|
|
152
|
+
@functools.lru_cache()
|
|
153
|
+
def _get_group_id(self, path) -> int:
|
|
154
|
+
group = self._make_requests_to_api("groups/%s", path, "GET")
|
|
155
|
+
return int(group["id"])
|
|
156
|
+
|
|
157
|
+
@functools.lru_cache()
|
|
158
|
+
def _get_protected_branch_id(self, project_and_group_name, branch) -> int:
|
|
159
|
+
branch = self._make_requests_to_api("projects/%s/protected_branches/%s", (project_and_group_name, branch))
|
|
160
|
+
return int(branch["id"])
|
|
161
|
+
|
|
162
|
+
@functools.lru_cache()
|
|
163
|
+
def _get_project_id(self, project_and_group):
|
|
164
|
+
# This is a NEW workaround for https://github.com/gitlabhq/gitlabhq/issues/8290
|
|
165
|
+
result = self.get_project(project_and_group)
|
|
166
|
+
return str(result["id"])
|
|
167
|
+
|
|
168
|
+
def _make_requests_to_api(
|
|
169
|
+
self,
|
|
170
|
+
path_as_format_string,
|
|
171
|
+
args=None,
|
|
172
|
+
method="GET",
|
|
173
|
+
data=None,
|
|
174
|
+
expected_codes=200,
|
|
175
|
+
json=None,
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
Makes an HTTP request or requests to the GitLab API endpoint. More than one request is made automatically
|
|
179
|
+
if the endpoint is paginated. (See underlying method for authentication, retries, timeout etc.)
|
|
180
|
+
|
|
181
|
+
:param path_as_format_string: path with parts to be replaced by values from `args` replaced by '%s'
|
|
182
|
+
(aka the old-style Python string formatting, see:
|
|
183
|
+
https://docs.python.org/2/library/stdtypes.html#string-formatting )
|
|
184
|
+
:param args: single element or a tuple of values to put under '%s's in `path_as_format_string`
|
|
185
|
+
:param method: uppercase string of a HTTP method name, like 'GET' or 'PUT'
|
|
186
|
+
:param data: dict with data to be 'PUT'ted or 'POST'ed
|
|
187
|
+
:param expected_codes: a single HTTP code (like: 200) or a list of accepted HTTP codes
|
|
188
|
+
- if the call to the API will return other code an exception will be thrown
|
|
189
|
+
:param json: alternatively to `dict` you can set this to a string that can be parsed as JSON that will
|
|
190
|
+
be used as data to be 'PUT'ted or 'POST'ed
|
|
191
|
+
:return: data returned by the endpoint, as a JSON object. If the API is paginated, it returns JSONs with
|
|
192
|
+
arrays of objects and then this method returns JSON with a single array that contains all of those
|
|
193
|
+
objects.
|
|
194
|
+
"""
|
|
195
|
+
if method != "GET":
|
|
196
|
+
response = self._make_request_to_api(path_as_format_string, args, method, data, expected_codes, json)
|
|
197
|
+
return response.json()
|
|
198
|
+
else:
|
|
199
|
+
if "?" in path_as_format_string:
|
|
200
|
+
path_as_format_string += "&per_page=100"
|
|
201
|
+
else:
|
|
202
|
+
path_as_format_string += "?per_page=100"
|
|
203
|
+
|
|
204
|
+
first_response = self._make_request_to_api(path_as_format_string, args, method, data, expected_codes, json)
|
|
205
|
+
results = first_response.json()
|
|
206
|
+
|
|
207
|
+
# In newer versions of GitLab the 'x-total-pages' may not be available
|
|
208
|
+
# anymore, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43159
|
|
209
|
+
# so let's use the 'x-next-page' header instead
|
|
210
|
+
|
|
211
|
+
response = first_response
|
|
212
|
+
while True:
|
|
213
|
+
if "x-next-page" in response.headers and response.headers["x-next-page"]:
|
|
214
|
+
next_page = response.headers["x-next-page"]
|
|
215
|
+
response = self._make_request_to_api(
|
|
216
|
+
path_as_format_string + "&page=" + str(next_page),
|
|
217
|
+
args,
|
|
218
|
+
method,
|
|
219
|
+
data,
|
|
220
|
+
expected_codes,
|
|
221
|
+
json,
|
|
222
|
+
)
|
|
223
|
+
results += response.json()
|
|
224
|
+
else:
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
return results
|
|
228
|
+
|
|
229
|
+
def _make_request_to_api(self, path_as_format_string, args, method, dict_data, expected_codes, json_data):
|
|
230
|
+
"""
|
|
231
|
+
Makes a single request to the GitLab API. Takes care of the authentication, basic error processing,
|
|
232
|
+
retries, timeout etc.
|
|
233
|
+
|
|
234
|
+
:param for the params description please see `_make_requests_to_api()`
|
|
235
|
+
:return: data returned by the endpoint, as a JSON object.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
expected_codes = self._listify(expected_codes)
|
|
239
|
+
|
|
240
|
+
if dict_data and json_data:
|
|
241
|
+
raise Exception("You need to pass the data either as dict (dict_data) or JSON (json_data), not both!")
|
|
242
|
+
|
|
243
|
+
url = f"{self.gitlab_config['url']}/api/v4/{self._format_with_url_encoding(path_as_format_string, args)}"
|
|
244
|
+
if dict_data:
|
|
245
|
+
response = self.session.request(method, url, data=dict_data, timeout=self.gitlab_config["timeout"])
|
|
246
|
+
debug(f"===> data = {to_str(dict_data)}")
|
|
247
|
+
elif json_data:
|
|
248
|
+
response = self.session.request(method, url, json=json_data, timeout=self.gitlab_config["timeout"])
|
|
249
|
+
debug(f"===> json = {to_str(json_data)}")
|
|
250
|
+
else:
|
|
251
|
+
response = self.session.request(method, url, timeout=self.gitlab_config["timeout"])
|
|
252
|
+
|
|
253
|
+
if response.status_code in expected_codes:
|
|
254
|
+
# if we accept error responses then they will likely not contain a JSON body
|
|
255
|
+
# so fake it to fix further calls to response.json()
|
|
256
|
+
if response.status_code == 204 or (400 <= response.status_code <= 499):
|
|
257
|
+
response.json = lambda: {}
|
|
258
|
+
else:
|
|
259
|
+
if response.status_code == 404:
|
|
260
|
+
raise NotFoundException(f"Resource with url='{url}' not found (HTTP 404)!")
|
|
261
|
+
else:
|
|
262
|
+
if dict_data:
|
|
263
|
+
data_output = f"data='{to_str(dict_data)}' "
|
|
264
|
+
elif json_data:
|
|
265
|
+
data_output = f"json='{to_str(json_data)}' "
|
|
266
|
+
else:
|
|
267
|
+
data_output = ""
|
|
268
|
+
|
|
269
|
+
raise UnexpectedResponseException(
|
|
270
|
+
f"Request url='{url}', method={method}, {data_output}failed -"
|
|
271
|
+
f" expected code(s) {str(expected_codes)},"
|
|
272
|
+
f" got code {response.status_code} & body: '{response.text}'",
|
|
273
|
+
response.status_code,
|
|
274
|
+
response.text,
|
|
275
|
+
)
|
|
276
|
+
if response.json():
|
|
277
|
+
debug(f"<--- json = {to_str(response.json())}")
|
|
278
|
+
else:
|
|
279
|
+
debug(f"<--- json = (empty))")
|
|
280
|
+
return response
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _format_with_url_encoding(format_string, single_arg_or_args_tuple):
|
|
284
|
+
# we want to URL-encode all the args, but not the path itself which looks like "/foo/%s/bar"
|
|
285
|
+
# because '/'s here are NOT to be URL-encoded
|
|
286
|
+
|
|
287
|
+
if not single_arg_or_args_tuple:
|
|
288
|
+
# there are no params, so the format_string is the URL
|
|
289
|
+
return format_string
|
|
290
|
+
else:
|
|
291
|
+
if type(single_arg_or_args_tuple) == tuple:
|
|
292
|
+
# URL-encode each arg in the tuple and return it as tuple too
|
|
293
|
+
url_encoded_args = ()
|
|
294
|
+
for arg in single_arg_or_args_tuple:
|
|
295
|
+
url_encoded_args += (parse.quote_plus(str(arg)),)
|
|
296
|
+
else:
|
|
297
|
+
# URL-encode single arg
|
|
298
|
+
url_encoded_args = parse.quote_plus(str(single_arg_or_args_tuple))
|
|
299
|
+
|
|
300
|
+
return format_string % url_encoded_args
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _listify(expected_codes):
|
|
304
|
+
if isinstance(expected_codes, int):
|
|
305
|
+
return [expected_codes]
|
|
306
|
+
else:
|
|
307
|
+
return expected_codes
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestRequestFailedException(Exception):
|
|
311
|
+
def __init__(self, underlying: Exception):
|
|
312
|
+
self.underlying = underlying
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class NotFoundException(Exception):
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class TimeoutWaitingForDeletion(Exception):
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class InvalidParametersException(Exception):
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class UnexpectedResponseException(Exception):
|
|
328
|
+
def __init__(self, message: str, response_status_code: int, response_text: str):
|
|
329
|
+
self.message: str = message
|
|
330
|
+
self.response_status_code: int = response_status_code
|
|
331
|
+
self.response_text: str = response_text
|
|
332
|
+
|
|
333
|
+
def __str__(self):
|
|
334
|
+
return self.message
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from gitlabform.gitlab.groups import GitLabGroups
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GitLabGroupBadges(GitLabGroups):
|
|
5
|
+
def get_group_badges(self, group_path):
|
|
6
|
+
# (unlike the one for project badges) this endpoint returns ONLY group badges
|
|
7
|
+
return self._make_requests_to_api(
|
|
8
|
+
"groups/%s/badges",
|
|
9
|
+
group_path,
|
|
10
|
+
expected_codes=200,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def add_group_badge(
|
|
14
|
+
self,
|
|
15
|
+
group_path,
|
|
16
|
+
badge_in_config,
|
|
17
|
+
):
|
|
18
|
+
return self._make_requests_to_api(
|
|
19
|
+
"groups/%s/badges",
|
|
20
|
+
group_path,
|
|
21
|
+
method="POST",
|
|
22
|
+
data=badge_in_config,
|
|
23
|
+
expected_codes=201,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def edit_group_badge(
|
|
27
|
+
self,
|
|
28
|
+
group_path,
|
|
29
|
+
badge_in_gitlab,
|
|
30
|
+
badge_in_config,
|
|
31
|
+
):
|
|
32
|
+
return self._make_requests_to_api(
|
|
33
|
+
"groups/%s/badges/%s",
|
|
34
|
+
(group_path, badge_in_gitlab["id"]),
|
|
35
|
+
method="PUT",
|
|
36
|
+
data=badge_in_config,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def delete_group_badge(
|
|
40
|
+
self,
|
|
41
|
+
group_path,
|
|
42
|
+
badge_in_gitlab,
|
|
43
|
+
):
|
|
44
|
+
# 404 means it is already removed, so let's accept it for idempotency
|
|
45
|
+
return self._make_requests_to_api(
|
|
46
|
+
"groups/%s/badges/%s",
|
|
47
|
+
(group_path, badge_in_gitlab["id"]),
|
|
48
|
+
method="DELETE",
|
|
49
|
+
expected_codes=[200, 204, 404],
|
|
50
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from gitlabform.gitlab.core import NotFoundException, InvalidParametersException
|
|
2
|
+
from gitlabform.gitlab.groups import GitLabGroups
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GitLabGroupLDAPLinks(GitLabGroups):
|
|
6
|
+
def get_ldap_group_links(self, group):
|
|
7
|
+
group_id = self.get_group_id_case_insensitive(group)
|
|
8
|
+
return self._make_requests_to_api("groups/%s/ldap_group_links", group_id, expected_codes=[200, 404])
|
|
9
|
+
|
|
10
|
+
def add_ldap_group_link(self, group, data):
|
|
11
|
+
group_id = self.get_group_id_case_insensitive(group)
|
|
12
|
+
data["id"] = group_id
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
return self._make_requests_to_api(
|
|
16
|
+
"groups/%s/ldap_group_links",
|
|
17
|
+
group_id,
|
|
18
|
+
method="POST",
|
|
19
|
+
data=data,
|
|
20
|
+
expected_codes=[200, 201],
|
|
21
|
+
)
|
|
22
|
+
# this is a GitLab API bug - it returns 404 here instead of 400 for bad requests...
|
|
23
|
+
except NotFoundException:
|
|
24
|
+
raise InvalidParametersException(f"Invalid parameters for a Group LDAP link for group {group}: {data}")
|
|
25
|
+
|
|
26
|
+
def delete_ldap_group_link(self, group, data):
|
|
27
|
+
if "group_access" in data:
|
|
28
|
+
del data["group_access"]
|
|
29
|
+
|
|
30
|
+
group_id = self.get_group_id_case_insensitive(group)
|
|
31
|
+
data["id"] = group_id
|
|
32
|
+
|
|
33
|
+
# 404 means that the LDAP group link is already removed, so let's accept it for idempotency
|
|
34
|
+
self._make_requests_to_api(
|
|
35
|
+
"groups/%s/ldap_group_links",
|
|
36
|
+
group_id,
|
|
37
|
+
method="DELETE",
|
|
38
|
+
data=data,
|
|
39
|
+
expected_codes=[204, 404],
|
|
40
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
from gitlabform.gitlab.core import GitLabCore, NotFoundException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GitLabGroups(GitLabCore):
|
|
7
|
+
@functools.lru_cache()
|
|
8
|
+
def get_group_id_case_insensitive(self, some_string):
|
|
9
|
+
# Cache the mapping from some_string -> id, as that won't change during our run.
|
|
10
|
+
return self.get_group_case_insensitive(some_string)["id"]
|
|
11
|
+
|
|
12
|
+
def get_group_case_insensitive(self, some_string):
|
|
13
|
+
# maybe "foo/bar" is some group's path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
# try with exact case
|
|
17
|
+
return self.get_group(some_string)
|
|
18
|
+
except NotFoundException:
|
|
19
|
+
# try case insensitive
|
|
20
|
+
groups = self._make_requests_to_api(
|
|
21
|
+
"groups?search=%s",
|
|
22
|
+
some_string.lower(),
|
|
23
|
+
method="GET",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for group in groups:
|
|
27
|
+
if group["full_path"].lower() == some_string.lower():
|
|
28
|
+
return group
|
|
29
|
+
raise NotFoundException(f"Group/subgroup with path '{some_string}' not found.")
|
|
30
|
+
|
|
31
|
+
def get_group(self, name):
|
|
32
|
+
return self._make_requests_to_api("groups/%s", name)
|
|
33
|
+
|
|
34
|
+
def get_group_descendants(self, group_id_or_path):
|
|
35
|
+
return self._make_requests_to_api("groups/%s/descendant_groups", group_id_or_path)
|
|
36
|
+
|
|
37
|
+
def get_groups(self):
|
|
38
|
+
"""
|
|
39
|
+
:return: sorted list of groups
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if self.admin:
|
|
43
|
+
query = "all_available=true"
|
|
44
|
+
else:
|
|
45
|
+
# as a non-admin it's pointless to get groups with a lower role than Reporter
|
|
46
|
+
# - it's the minimal role that is needed to manage something (f.e. group labels)
|
|
47
|
+
query = f"min_access_level=20"
|
|
48
|
+
|
|
49
|
+
result = self._make_requests_to_api(f"groups?{query}")
|
|
50
|
+
|
|
51
|
+
return sorted(map(lambda x: x["full_path"], result))
|
|
52
|
+
|
|
53
|
+
def get_projects(self, group, include_archived=False, only_names=True):
|
|
54
|
+
"""
|
|
55
|
+
:param group: group name
|
|
56
|
+
:param include_archived: set to True if archived projects should also be returned
|
|
57
|
+
:param only_names: set to False to get the whole project objects
|
|
58
|
+
:return: sorted list of strings "group/project_name". Note that only projects from "group" namespace are
|
|
59
|
+
returned, so if "group" (= members of this group) is also a member of some projects, they won't be
|
|
60
|
+
returned here.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
# there are 3 states of the "archived" flag: true, false, undefined
|
|
64
|
+
# we use the last 2
|
|
65
|
+
if include_archived:
|
|
66
|
+
query1 = "include_subgroups=true"
|
|
67
|
+
else:
|
|
68
|
+
query1 = "include_subgroups=true&archived=false"
|
|
69
|
+
|
|
70
|
+
if self.admin:
|
|
71
|
+
query2 = "all_available=true"
|
|
72
|
+
else:
|
|
73
|
+
# it's pointless to get projects with a lower role than Reporter
|
|
74
|
+
# - it's the minimal role that is needed to manage something (f.e. labels)
|
|
75
|
+
query2 = f"min_access_level=20"
|
|
76
|
+
|
|
77
|
+
projects = self._make_requests_to_api(f"groups/%s/projects?{query1}&{query2}", group)
|
|
78
|
+
except NotFoundException:
|
|
79
|
+
projects = []
|
|
80
|
+
|
|
81
|
+
project_and_groups_in_group_namespace = [
|
|
82
|
+
project for project in projects if project["path_with_namespace"].startswith(group + "/")
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if only_names:
|
|
86
|
+
return sorted(
|
|
87
|
+
map(
|
|
88
|
+
lambda x: x["path_with_namespace"],
|
|
89
|
+
project_and_groups_in_group_namespace,
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
return sorted(
|
|
94
|
+
project_and_groups_in_group_namespace,
|
|
95
|
+
key=lambda x: x["path_with_namespace"],
|
|
96
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from gitlabform.gitlab.core import GitLabCore
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GitLabMergeRequests(GitLabCore):
|
|
5
|
+
def create_mr(
|
|
6
|
+
self,
|
|
7
|
+
project_and_group_name,
|
|
8
|
+
source_branch,
|
|
9
|
+
target_branch,
|
|
10
|
+
title,
|
|
11
|
+
description=None,
|
|
12
|
+
):
|
|
13
|
+
data = {
|
|
14
|
+
"id": project_and_group_name,
|
|
15
|
+
"source_branch": source_branch,
|
|
16
|
+
"target_branch": target_branch,
|
|
17
|
+
"title": title,
|
|
18
|
+
"description": description,
|
|
19
|
+
}
|
|
20
|
+
return self._make_requests_to_api(
|
|
21
|
+
"projects/%s/merge_requests",
|
|
22
|
+
project_and_group_name,
|
|
23
|
+
method="POST",
|
|
24
|
+
data=data,
|
|
25
|
+
expected_codes=201,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def accept_mr(self, project_and_group_name, mr_iid):
|
|
29
|
+
return self._make_requests_to_api(
|
|
30
|
+
"projects/%s/merge_requests/%s/merge",
|
|
31
|
+
(project_and_group_name, mr_iid),
|
|
32
|
+
method="PUT",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def update_mr(self, project_and_group_name, mr_iid, data):
|
|
36
|
+
self._make_requests_to_api(
|
|
37
|
+
"projects/%s/merge_requests/%s",
|
|
38
|
+
(project_and_group_name, mr_iid),
|
|
39
|
+
method="PUT",
|
|
40
|
+
data=data,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get_mrs(self, project_and_group_name):
|
|
44
|
+
"""
|
|
45
|
+
:param project_and_group_name: like 'group/project'
|
|
46
|
+
:return: get all *open* MRs in given project
|
|
47
|
+
"""
|
|
48
|
+
return self._make_requests_to_api(
|
|
49
|
+
"projects/%s/merge_requests?scope=all&state=opened",
|
|
50
|
+
project_and_group_name,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_mr(self, project_and_group_name, mr_iid):
|
|
54
|
+
return self._make_requests_to_api("projects/%s/merge_requests/%s", (project_and_group_name, mr_iid))
|
|
55
|
+
|
|
56
|
+
def get_mr_approvals(self, project_and_group_name, mr_iid):
|
|
57
|
+
return self._make_requests_to_api("projects/%s/merge_requests/%s/approvals", (project_and_group_name, mr_iid))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from gitlabform.gitlab.core import GitLabCore
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GitLabPipelines(GitLabCore):
|
|
5
|
+
def get_pipelines(self, project_and_group_name, branch):
|
|
6
|
+
pipelines = self._make_requests_to_api(
|
|
7
|
+
"projects/%s/pipelines?ref=%s",
|
|
8
|
+
(project_and_group_name, branch),
|
|
9
|
+
)
|
|
10
|
+
return pipelines
|
|
11
|
+
|
|
12
|
+
def get_pipeline(self, project_and_group_name, pipeline_id):
|
|
13
|
+
pipeline = self._make_requests_to_api("/projects/%s/pipelines/%s", (project_and_group_name, pipeline_id))
|
|
14
|
+
return pipeline
|
|
15
|
+
|
|
16
|
+
def retry_pipeline(self, project_and_group_name, pipeline_id):
|
|
17
|
+
pipeline = self._make_requests_to_api(
|
|
18
|
+
"projects/%s/pipelines/%s/retry",
|
|
19
|
+
(project_and_group_name, pipeline_id),
|
|
20
|
+
method="POST",
|
|
21
|
+
expected_codes=[200, 201],
|
|
22
|
+
)
|
|
23
|
+
return pipeline
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from gitlabform.gitlab.projects import GitLabProjects
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GitLabProjectBadges(GitLabProjects):
|
|
5
|
+
def get_project_badges(self, project_and_group_name):
|
|
6
|
+
badges = self._make_requests_to_api(
|
|
7
|
+
"projects/%s/badges",
|
|
8
|
+
project_and_group_name,
|
|
9
|
+
expected_codes=200,
|
|
10
|
+
)
|
|
11
|
+
# according to the docs_new this endpoint returns also the group badges
|
|
12
|
+
# but we want only the project badges here
|
|
13
|
+
return [badge for badge in badges if badge["kind"] == "project"]
|
|
14
|
+
|
|
15
|
+
def add_project_badge(
|
|
16
|
+
self,
|
|
17
|
+
project_and_group_name,
|
|
18
|
+
badge_in_config,
|
|
19
|
+
):
|
|
20
|
+
return self._make_requests_to_api(
|
|
21
|
+
"projects/%s/badges",
|
|
22
|
+
project_and_group_name,
|
|
23
|
+
method="POST",
|
|
24
|
+
data=badge_in_config,
|
|
25
|
+
expected_codes=201,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def edit_project_badge(
|
|
29
|
+
self,
|
|
30
|
+
project_and_group_name,
|
|
31
|
+
badge_in_gitlab,
|
|
32
|
+
badge_in_config,
|
|
33
|
+
):
|
|
34
|
+
return self._make_requests_to_api(
|
|
35
|
+
"projects/%s/badges/%s",
|
|
36
|
+
(project_and_group_name, badge_in_gitlab["id"]),
|
|
37
|
+
method="PUT",
|
|
38
|
+
data=badge_in_config,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def delete_project_badge(
|
|
42
|
+
self,
|
|
43
|
+
project_and_group_name,
|
|
44
|
+
badge_in_gitlab,
|
|
45
|
+
):
|
|
46
|
+
# 404 means it is already removed, so let's accept it for idempotency
|
|
47
|
+
return self._make_requests_to_api(
|
|
48
|
+
"projects/%s/badges/%s",
|
|
49
|
+
(project_and_group_name, badge_in_gitlab["id"]),
|
|
50
|
+
method="DELETE",
|
|
51
|
+
expected_codes=[200, 204, 404],
|
|
52
|
+
)
|