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
gitlabform/__init__.py
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import debug, critical, error, warning, info
|
|
3
|
+
|
|
4
|
+
# Use Rich to make logs have a colorized and formatted output
|
|
5
|
+
from rich.logging import RichHandler
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import luddite
|
|
11
|
+
from importlib.metadata import version as package_version
|
|
12
|
+
import textwrap
|
|
13
|
+
import traceback
|
|
14
|
+
|
|
15
|
+
from packaging import version
|
|
16
|
+
from typing import Any, Tuple
|
|
17
|
+
|
|
18
|
+
from gitlabform.configuration import Configuration
|
|
19
|
+
from gitlabform.configuration.core import (
|
|
20
|
+
ConfigFileNotFoundException,
|
|
21
|
+
ConfigInvalidException,
|
|
22
|
+
)
|
|
23
|
+
from gitlabform.configuration.transform import ConfigurationTransformers
|
|
24
|
+
from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
|
|
25
|
+
from gitlabform.gitlab import GitLab
|
|
26
|
+
from gitlabform.gitlab.core import TestRequestFailedException
|
|
27
|
+
from gitlabform.lists import Entities
|
|
28
|
+
from gitlabform.lists.filter import GroupsAndProjectsFilters
|
|
29
|
+
from gitlabform.lists.groups import GroupsProvider
|
|
30
|
+
from gitlabform.lists.projects import ProjectsProvider
|
|
31
|
+
from gitlabform.output import EffectiveConfigurationFile
|
|
32
|
+
from gitlabform.processors.application import ApplicationProcessors
|
|
33
|
+
from gitlabform.processors.group import GroupProcessors
|
|
34
|
+
from gitlabform.processors.project import ProjectProcessors
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GitLabForm:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
include_archived_projects=True,
|
|
43
|
+
include_projects_scheduled_for_deletion=True,
|
|
44
|
+
target=None,
|
|
45
|
+
config_string=None,
|
|
46
|
+
noop=False,
|
|
47
|
+
output_file=None,
|
|
48
|
+
recurse_subgroups=True,
|
|
49
|
+
):
|
|
50
|
+
if target and config_string:
|
|
51
|
+
# this mode is basically only for testing
|
|
52
|
+
|
|
53
|
+
self.target = target
|
|
54
|
+
self.config_string = config_string
|
|
55
|
+
self.verbose = True
|
|
56
|
+
self.debug = True
|
|
57
|
+
self.strict = True
|
|
58
|
+
self.start_from = 1
|
|
59
|
+
self.start_from_group = 1
|
|
60
|
+
self.noop = noop
|
|
61
|
+
self.diff_only_changed = False
|
|
62
|
+
self.output_file = output_file
|
|
63
|
+
self.skip_version_check = True
|
|
64
|
+
self.include_archived_projects = include_archived_projects
|
|
65
|
+
self.include_projects_scheduled_for_deletion = include_projects_scheduled_for_deletion
|
|
66
|
+
self.just_show_version = False
|
|
67
|
+
self.terminate_after_error = True
|
|
68
|
+
self.only_sections = "all"
|
|
69
|
+
self.exclude_sections = []
|
|
70
|
+
self.recurse_subgroups = recurse_subgroups
|
|
71
|
+
|
|
72
|
+
self._configure_output(tests=True)
|
|
73
|
+
else:
|
|
74
|
+
# normal mode
|
|
75
|
+
|
|
76
|
+
(
|
|
77
|
+
self.target,
|
|
78
|
+
self.config,
|
|
79
|
+
self.verbose,
|
|
80
|
+
self.debug,
|
|
81
|
+
self.strict,
|
|
82
|
+
self.start_from,
|
|
83
|
+
self.start_from_group,
|
|
84
|
+
self.noop,
|
|
85
|
+
self.diff_only_changed,
|
|
86
|
+
self.output_file,
|
|
87
|
+
self.skip_version_check,
|
|
88
|
+
self.include_archived_projects,
|
|
89
|
+
self.include_projects_scheduled_for_deletion,
|
|
90
|
+
self.just_show_version,
|
|
91
|
+
self.terminate_after_error,
|
|
92
|
+
self.only_sections,
|
|
93
|
+
self.exclude_sections,
|
|
94
|
+
self.recurse_subgroups,
|
|
95
|
+
) = self._parse_args()
|
|
96
|
+
|
|
97
|
+
self._configure_output()
|
|
98
|
+
|
|
99
|
+
self._show_version(self.skip_version_check)
|
|
100
|
+
if self.just_show_version:
|
|
101
|
+
sys.exit(0)
|
|
102
|
+
|
|
103
|
+
if not self.target:
|
|
104
|
+
critical("target parameter is required.")
|
|
105
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
106
|
+
|
|
107
|
+
self.gitlab, self.configuration = self._initialize_configuration_and_gitlab()
|
|
108
|
+
|
|
109
|
+
self.application_processors = ApplicationProcessors(self.gitlab, self.configuration, self.strict)
|
|
110
|
+
self.group_processors = GroupProcessors(self.gitlab, self.configuration, self.strict)
|
|
111
|
+
self.project_processors = ProjectProcessors(self.gitlab, self.configuration, self.strict)
|
|
112
|
+
self.groups_provider = GroupsProvider(
|
|
113
|
+
self.gitlab,
|
|
114
|
+
self.configuration,
|
|
115
|
+
self.recurse_subgroups,
|
|
116
|
+
)
|
|
117
|
+
self.projects_provider = ProjectsProvider(
|
|
118
|
+
self.gitlab,
|
|
119
|
+
self.configuration,
|
|
120
|
+
self.include_archived_projects,
|
|
121
|
+
self.include_projects_scheduled_for_deletion,
|
|
122
|
+
self.recurse_subgroups,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.groups_and_projects_filters = GroupsAndProjectsFilters(
|
|
126
|
+
self.configuration, self.group_processors, self.project_processors
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _parse_args() -> Tuple:
|
|
131
|
+
"""
|
|
132
|
+
Parses the input command-line arguments.
|
|
133
|
+
|
|
134
|
+
:return: a tuple with all the arguments that have been parsed
|
|
135
|
+
"""
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
description=textwrap.dedent(f"""
|
|
138
|
+
🏗 Specialized configuration as a code tool for GitLab projects, groups and more
|
|
139
|
+
using hierarchical configuration written in YAML.
|
|
140
|
+
|
|
141
|
+
Exits with code:
|
|
142
|
+
* 0 - on success,
|
|
143
|
+
* {EXIT_INVALID_INPUT} - on invalid input errors (f.e. bad syntax in the config file) ~ "it's your fault". 😅
|
|
144
|
+
* {EXIT_PROCESSING_ERROR} - if there were backend processing errors (f.e. when requests to GitLab fail) ~ "it's not your fault". 😎
|
|
145
|
+
"""),
|
|
146
|
+
formatter_class=Formatter,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
parser.add_argument(
|
|
150
|
+
"target",
|
|
151
|
+
nargs="?",
|
|
152
|
+
help='Project name in "group/project" format \n'
|
|
153
|
+
"OR a single group name \n"
|
|
154
|
+
'OR "ALL_DEFINED" to run for all groups and projects defined the config \n'
|
|
155
|
+
'OR "ALL" to run for all projects that you have access to',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"-V",
|
|
160
|
+
"--version",
|
|
161
|
+
dest="just_show_version",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="show version and exit",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"-k",
|
|
168
|
+
"--skip-version-check",
|
|
169
|
+
dest="skip_version_check",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Skips checking if the latest version is used",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
parser.add_argument("-c", "--config", default="config.yml", help="config file path and filename")
|
|
175
|
+
|
|
176
|
+
verbosity_args = parser.add_mutually_exclusive_group()
|
|
177
|
+
|
|
178
|
+
verbosity_args.add_argument("-v", "--verbose", action="store_true", help="verbose output")
|
|
179
|
+
|
|
180
|
+
verbosity_args.add_argument(
|
|
181
|
+
"-d",
|
|
182
|
+
"--debug",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="debug output (!!! WARNING !!!: sensitive data and secrets may"
|
|
185
|
+
" be printed in this mode - all the data sent to GitLab API"
|
|
186
|
+
" will be printed in plain-text.)",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"-s",
|
|
191
|
+
"--strict",
|
|
192
|
+
action="store_true",
|
|
193
|
+
help="stop on missing branches and tags",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
parser.add_argument(
|
|
197
|
+
"-n",
|
|
198
|
+
"--noop",
|
|
199
|
+
dest="noop",
|
|
200
|
+
action="store_true",
|
|
201
|
+
help="run in no-op (dry run) mode",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"-doc",
|
|
206
|
+
"--diff-only-changed",
|
|
207
|
+
dest="diff_only_changed",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="only show items who's values are changing in the diff.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"-o",
|
|
214
|
+
"--output-file",
|
|
215
|
+
dest="output_file",
|
|
216
|
+
default=None,
|
|
217
|
+
help="name/path of a file to write the effective configs to"
|
|
218
|
+
" (!!! WARNING !!!: if your config contains sensitive data or secrets, then this file will also"
|
|
219
|
+
" contain them.)",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"-a",
|
|
224
|
+
"--include-archived-projects",
|
|
225
|
+
dest="include_archived_projects",
|
|
226
|
+
action="store_true",
|
|
227
|
+
help="Includes processing projects that are archived",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--include-projects-scheduled-for-deletion",
|
|
232
|
+
dest="include_projects_scheduled_for_deletion",
|
|
233
|
+
action="store_true",
|
|
234
|
+
help="Includes processing projects that are scheduled for deletion",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"-t",
|
|
239
|
+
"--terminate",
|
|
240
|
+
dest="terminate_after_error",
|
|
241
|
+
action="store_true",
|
|
242
|
+
help=f"exit with {EXIT_PROCESSING_ERROR} after the first group/project processing error."
|
|
243
|
+
f" (default: process all the requested groups/projects and skip the failing ones."
|
|
244
|
+
f" At the end, if there were any groups/projects, exit with {EXIT_PROCESSING_ERROR}.)",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"-sf",
|
|
249
|
+
"--start-from",
|
|
250
|
+
dest="start_from",
|
|
251
|
+
default=1,
|
|
252
|
+
type=int,
|
|
253
|
+
help="start processing projects from the given one"
|
|
254
|
+
' (as numbered by "x/y Processing group/project" messages)',
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
parser.add_argument(
|
|
258
|
+
"-sfg",
|
|
259
|
+
"--start-from-group",
|
|
260
|
+
dest="start_from_group",
|
|
261
|
+
default=1,
|
|
262
|
+
type=int,
|
|
263
|
+
help="start processing groups from the given one "
|
|
264
|
+
'(as numbered by "x/y Processing group/project" messages)',
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
sections_group = parser.add_mutually_exclusive_group()
|
|
268
|
+
|
|
269
|
+
sections_group.add_argument(
|
|
270
|
+
"-os",
|
|
271
|
+
"--only-sections",
|
|
272
|
+
dest="only_sections",
|
|
273
|
+
default="all",
|
|
274
|
+
type=str,
|
|
275
|
+
help="process only section with these names (comma-delimited)",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
sections_group.add_argument(
|
|
279
|
+
"-es",
|
|
280
|
+
"--exclude-sections",
|
|
281
|
+
dest="exclude_sections",
|
|
282
|
+
default="none",
|
|
283
|
+
type=str,
|
|
284
|
+
help="exclude sections with these names (comma-delimited). Warning: may result in failure or odd functionality when excluding sections that are dependent on by other sections.",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"-r",
|
|
289
|
+
"--recurse-subgroups",
|
|
290
|
+
dest="recurse_subsgroups",
|
|
291
|
+
action="store_true",
|
|
292
|
+
help="include all subgroups recursively. Always true for ALL, ALL_DEFINED",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
args = parser.parse_args()
|
|
296
|
+
|
|
297
|
+
if args.only_sections != "all":
|
|
298
|
+
args.only_sections = args.only_sections.split(",")
|
|
299
|
+
|
|
300
|
+
if args.exclude_sections != "none":
|
|
301
|
+
args.exclude_sections = args.exclude_sections.split(",")
|
|
302
|
+
else:
|
|
303
|
+
args.exclude_sections = []
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
args.target,
|
|
307
|
+
args.config,
|
|
308
|
+
args.verbose,
|
|
309
|
+
args.debug,
|
|
310
|
+
args.strict,
|
|
311
|
+
args.start_from,
|
|
312
|
+
args.start_from_group,
|
|
313
|
+
args.noop,
|
|
314
|
+
args.diff_only_changed,
|
|
315
|
+
args.output_file,
|
|
316
|
+
args.skip_version_check,
|
|
317
|
+
args.include_archived_projects,
|
|
318
|
+
args.include_projects_scheduled_for_deletion,
|
|
319
|
+
args.just_show_version,
|
|
320
|
+
args.terminate_after_error,
|
|
321
|
+
args.only_sections,
|
|
322
|
+
args.exclude_sections,
|
|
323
|
+
args.recurse_subsgroups,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def _configure_output(self, tests=False) -> None:
|
|
327
|
+
"""
|
|
328
|
+
Configures the application output using logging, based on debug and verbose flags:
|
|
329
|
+
|
|
330
|
+
* normal mode - logging only WARNING logs
|
|
331
|
+
* verbose mode - logging INFO level and above
|
|
332
|
+
* debug / tests mode - logging DEBUG level and above, along with rich exception tracebacks (may expose secrets)
|
|
333
|
+
|
|
334
|
+
:param tests: True if we are running in tests mode
|
|
335
|
+
"""
|
|
336
|
+
rich_tracebacks = False
|
|
337
|
+
if tests or self.debug:
|
|
338
|
+
level = logging.DEBUG
|
|
339
|
+
rich_tracebacks = True
|
|
340
|
+
elif self.verbose:
|
|
341
|
+
level = logging.INFO
|
|
342
|
+
else:
|
|
343
|
+
level = logging.WARNING
|
|
344
|
+
|
|
345
|
+
logging.basicConfig(
|
|
346
|
+
level=level, format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=rich_tracebacks)]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _initialize_configuration_and_gitlab(self) -> Tuple[GitLab, Configuration]:
|
|
350
|
+
"""
|
|
351
|
+
Creates the GitLab object, which represents connection to the GitLab API,
|
|
352
|
+
and the configuration object, which represents the YAML configuration,
|
|
353
|
+
and runs processing the configuration with configuration transformers.
|
|
354
|
+
|
|
355
|
+
:return: tuple with GitLab and Configuration objects
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
if hasattr(self, "config_string"):
|
|
360
|
+
gitlab = GitLab(config_string=self.config_string)
|
|
361
|
+
else:
|
|
362
|
+
gitlab = GitLab(config_path=self.config)
|
|
363
|
+
configuration = gitlab.get_configuration()
|
|
364
|
+
|
|
365
|
+
configuration_transformers = ConfigurationTransformers(gitlab)
|
|
366
|
+
configuration_transformers.transform(configuration)
|
|
367
|
+
|
|
368
|
+
except ConfigFileNotFoundException as e:
|
|
369
|
+
critical(f"Config file not found at: {e}")
|
|
370
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
371
|
+
except ConfigInvalidException as e:
|
|
372
|
+
critical(f"Invalid config:\n{e.underlying}")
|
|
373
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
374
|
+
except TestRequestFailedException as e:
|
|
375
|
+
critical(f"GitLab test request failed:\n{e.underlying}")
|
|
376
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
377
|
+
|
|
378
|
+
return gitlab, configuration
|
|
379
|
+
|
|
380
|
+
def run(self) -> None:
|
|
381
|
+
"""
|
|
382
|
+
The main method.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
projects, groups = self._get_groups_and_projects(
|
|
386
|
+
self.target,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
group_number = 0
|
|
390
|
+
successful_groups = 0
|
|
391
|
+
failed_groups = {}
|
|
392
|
+
|
|
393
|
+
effective_configuration = EffectiveConfigurationFile(self.output_file)
|
|
394
|
+
|
|
395
|
+
application_configuration = self.configuration.get("application", {})
|
|
396
|
+
if application_configuration:
|
|
397
|
+
self.application_processors.process_entity(
|
|
398
|
+
"",
|
|
399
|
+
application_configuration,
|
|
400
|
+
dry_run=self.noop,
|
|
401
|
+
diff_only_changed=self.diff_only_changed,
|
|
402
|
+
effective_configuration=effective_configuration,
|
|
403
|
+
only_sections=self.only_sections,
|
|
404
|
+
exclude_sections=self.exclude_sections,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
for group in groups:
|
|
408
|
+
group_number += 1
|
|
409
|
+
|
|
410
|
+
if group_number < self.start_from_group:
|
|
411
|
+
self._info_group_count(
|
|
412
|
+
"@",
|
|
413
|
+
group_number,
|
|
414
|
+
len(groups),
|
|
415
|
+
"yellow",
|
|
416
|
+
f"Skipping group {group} as requested to start from {self.start_from_group}...",
|
|
417
|
+
)
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
group_configuration = self.configuration.get_effective_config_for_group(group)
|
|
421
|
+
|
|
422
|
+
effective_configuration.add_placeholder(group)
|
|
423
|
+
|
|
424
|
+
self._info_group_count(
|
|
425
|
+
"@",
|
|
426
|
+
group_number,
|
|
427
|
+
len(groups),
|
|
428
|
+
"black",
|
|
429
|
+
f"Processing group: {group}",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
self.group_processors.process_entity(
|
|
434
|
+
group,
|
|
435
|
+
group_configuration,
|
|
436
|
+
dry_run=self.noop,
|
|
437
|
+
diff_only_changed=self.diff_only_changed,
|
|
438
|
+
effective_configuration=effective_configuration,
|
|
439
|
+
only_sections=self.only_sections,
|
|
440
|
+
exclude_sections=self.exclude_sections,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
successful_groups += 1
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
failed_groups[group_number] = group
|
|
447
|
+
|
|
448
|
+
trace = traceback.format_exc()
|
|
449
|
+
message = f"Error occurred while processing group {group}, exception:\n\n{e}"
|
|
450
|
+
|
|
451
|
+
if self.terminate_after_error:
|
|
452
|
+
effective_configuration.write_to_file()
|
|
453
|
+
error(message)
|
|
454
|
+
debug(trace)
|
|
455
|
+
sys.exit(EXIT_PROCESSING_ERROR)
|
|
456
|
+
else:
|
|
457
|
+
warning(message)
|
|
458
|
+
debug(trace)
|
|
459
|
+
finally:
|
|
460
|
+
debug(f"@ ({group_number}/{len(groups)}) FINISHED Processing group: {group}")
|
|
461
|
+
|
|
462
|
+
project_number = 0
|
|
463
|
+
successful_projects = 0
|
|
464
|
+
failed_projects = {}
|
|
465
|
+
|
|
466
|
+
for project_and_group in projects:
|
|
467
|
+
project_number += 1
|
|
468
|
+
|
|
469
|
+
if project_number < self.start_from:
|
|
470
|
+
self._info_project_count(
|
|
471
|
+
"*",
|
|
472
|
+
project_number,
|
|
473
|
+
len(projects),
|
|
474
|
+
"yellow",
|
|
475
|
+
f"Skipping project {project_and_group} as requested to start from {self.start_from}...",
|
|
476
|
+
)
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
project_configuration = self.configuration.get_effective_config_for_project(project_and_group)
|
|
480
|
+
|
|
481
|
+
effective_configuration.add_placeholder(project_and_group)
|
|
482
|
+
|
|
483
|
+
self._info_project_count(
|
|
484
|
+
"*",
|
|
485
|
+
project_number,
|
|
486
|
+
len(projects),
|
|
487
|
+
"black",
|
|
488
|
+
f"Processing project: {project_and_group}",
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
self.project_processors.process_entity(
|
|
493
|
+
project_and_group,
|
|
494
|
+
project_configuration,
|
|
495
|
+
dry_run=self.noop,
|
|
496
|
+
diff_only_changed=self.diff_only_changed,
|
|
497
|
+
effective_configuration=effective_configuration,
|
|
498
|
+
only_sections=self.only_sections,
|
|
499
|
+
exclude_sections=self.exclude_sections,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
successful_projects += 1
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
if "Non GET methods are not allowed for moved projects" in str(e):
|
|
506
|
+
info("Project has been transferred, no need to process original location")
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
failed_projects[project_number] = project_and_group
|
|
510
|
+
|
|
511
|
+
trace = traceback.format_exc()
|
|
512
|
+
message = f"Error occurred while processing project {project_and_group}, exception:\n\n{e}"
|
|
513
|
+
|
|
514
|
+
if self.terminate_after_error:
|
|
515
|
+
effective_configuration.write_to_file()
|
|
516
|
+
error(message)
|
|
517
|
+
debug(trace)
|
|
518
|
+
sys.exit(EXIT_PROCESSING_ERROR)
|
|
519
|
+
else:
|
|
520
|
+
warning(message)
|
|
521
|
+
debug(trace)
|
|
522
|
+
finally:
|
|
523
|
+
debug(
|
|
524
|
+
f"* ({project_number}/{len(projects)})" f" FINISHED Processing project: {project_and_group}",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
effective_configuration.write_to_file()
|
|
528
|
+
|
|
529
|
+
self._show_summary(
|
|
530
|
+
groups,
|
|
531
|
+
projects,
|
|
532
|
+
successful_groups,
|
|
533
|
+
successful_projects,
|
|
534
|
+
failed_groups,
|
|
535
|
+
failed_projects,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@classmethod
|
|
539
|
+
def _show_version(cls, skip_version_check: bool) -> None:
|
|
540
|
+
"""
|
|
541
|
+
Prints the app version and how it relates to the latest stable version
|
|
542
|
+
available at PyPI.
|
|
543
|
+
|
|
544
|
+
:param skip_version_check: if True then the comparison to the latest versions is skipped
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
local_version = package_version("gitlabform")
|
|
548
|
+
|
|
549
|
+
version_text = ""
|
|
550
|
+
# Legacy windows console cannot support unicode emoji rendering via Rich
|
|
551
|
+
if not console.legacy_windows:
|
|
552
|
+
version_text = "🏗 "
|
|
553
|
+
version_text += f"GitLabForm version: [bold blue]{local_version}[/]"
|
|
554
|
+
|
|
555
|
+
if skip_version_check:
|
|
556
|
+
# just print version in use
|
|
557
|
+
console.print(version_text)
|
|
558
|
+
else:
|
|
559
|
+
try:
|
|
560
|
+
latest_version = luddite.get_version_pypi("gitlabform")
|
|
561
|
+
except Exception as e:
|
|
562
|
+
# end the line with current version
|
|
563
|
+
print()
|
|
564
|
+
error(f"Checking latest version failed:\n{e}")
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
latest_stable_text = "(the latest stable is {latest_version})"
|
|
568
|
+
|
|
569
|
+
if local_version == latest_version:
|
|
570
|
+
version_info = "= the latest stable"
|
|
571
|
+
# Legacy windows console cannot support unicode emoji rendering via Rich
|
|
572
|
+
if not console.legacy_windows:
|
|
573
|
+
version_info = f"{version_info} ☺️"
|
|
574
|
+
elif version.parse(local_version) < version.parse(latest_version):
|
|
575
|
+
version_info = f"= outdated, please update"
|
|
576
|
+
if not console.legacy_windows:
|
|
577
|
+
version_info = f"{version_info} 😔"
|
|
578
|
+
version_info = f"{version_info}! {latest_stable_text}"
|
|
579
|
+
else:
|
|
580
|
+
version_info = f"= pre-release: "
|
|
581
|
+
if not console.legacy_windows:
|
|
582
|
+
version_info = f"{version_info} 🤩"
|
|
583
|
+
version_info = f"{version_info} {latest_stable_text}"
|
|
584
|
+
|
|
585
|
+
# complete the line with a line ending
|
|
586
|
+
console.print(f"{version_text} {version_info}")
|
|
587
|
+
|
|
588
|
+
def _get_groups_and_projects(
|
|
589
|
+
self,
|
|
590
|
+
target: str,
|
|
591
|
+
) -> Tuple[list, list]:
|
|
592
|
+
"""
|
|
593
|
+
Gets the list of groups and projects to apply the configuration to, based on the provided 'target' parameter
|
|
594
|
+
and prints out the output header based on that.
|
|
595
|
+
|
|
596
|
+
:param target: what the configuration should be applied to
|
|
597
|
+
:return: a tuple of lists of effective projects and effective groups
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
if self.noop:
|
|
601
|
+
info("Running in dry-run mode...")
|
|
602
|
+
|
|
603
|
+
if target == "ALL":
|
|
604
|
+
info(">>> Getting ALL groups and projects that I have permission to modify...")
|
|
605
|
+
elif target == "ALL_DEFINED":
|
|
606
|
+
info(">>> Getting ALL groups and projects DEFINED in the configuration...")
|
|
607
|
+
else:
|
|
608
|
+
info(">>> Getting requested groups or projects...")
|
|
609
|
+
|
|
610
|
+
groups = self.groups_provider.get_groups(target)
|
|
611
|
+
projects = self.projects_provider.get_projects(target)
|
|
612
|
+
|
|
613
|
+
if len(groups.get_effective()) == 0 and len(projects.get_effective()) == 0:
|
|
614
|
+
if target == "ALL":
|
|
615
|
+
error_message = "GitLab has no projects and groups!"
|
|
616
|
+
elif target == "ALL_DEFINED":
|
|
617
|
+
error_message = "Configuration does not have any groups or projects defined!"
|
|
618
|
+
else:
|
|
619
|
+
error_message = f"Project or group {target} cannot be found in GitLab!"
|
|
620
|
+
critical(error_message)
|
|
621
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
622
|
+
|
|
623
|
+
self.groups_and_projects_filters.filter(groups, projects)
|
|
624
|
+
|
|
625
|
+
self._show_input_entities(groups)
|
|
626
|
+
self._show_input_entities(projects)
|
|
627
|
+
|
|
628
|
+
return projects.get_effective(), groups.get_effective()
|
|
629
|
+
|
|
630
|
+
@classmethod
|
|
631
|
+
def _show_input_entities(cls, entities: Entities) -> None:
|
|
632
|
+
"""
|
|
633
|
+
Prints out the groups or projects that will be processed.
|
|
634
|
+
|
|
635
|
+
:param entities: groups or projects
|
|
636
|
+
"""
|
|
637
|
+
info(f"# of {entities.name} to process: {len(entities.get_effective())}")
|
|
638
|
+
|
|
639
|
+
entities_omitted = ""
|
|
640
|
+
entities_verbose = f"{entities.name}: {entities.get_effective()}"
|
|
641
|
+
if entities.any_omitted():
|
|
642
|
+
entities_omitted += f"(# of omitted {entities.name} -"
|
|
643
|
+
first = True
|
|
644
|
+
for reason in entities.omitted:
|
|
645
|
+
if len(entities.omitted[reason]) > 0:
|
|
646
|
+
if not first:
|
|
647
|
+
entities_omitted += ","
|
|
648
|
+
entities_omitted += f" {reason.value}: {len(entities.omitted[reason])}"
|
|
649
|
+
entities_verbose += f"\nomitted {entities.name} - {reason.value}: {entities.get_omitted(reason)}"
|
|
650
|
+
first = False
|
|
651
|
+
entities_omitted += ")"
|
|
652
|
+
|
|
653
|
+
if entities_omitted:
|
|
654
|
+
info(entities_omitted)
|
|
655
|
+
|
|
656
|
+
info(entities_verbose)
|
|
657
|
+
|
|
658
|
+
@classmethod
|
|
659
|
+
def _show_summary(
|
|
660
|
+
cls,
|
|
661
|
+
effective_groups: list,
|
|
662
|
+
effective_projects: list,
|
|
663
|
+
successful_groups: int,
|
|
664
|
+
successful_projects: int,
|
|
665
|
+
failed_groups: dict,
|
|
666
|
+
failed_projects: dict,
|
|
667
|
+
):
|
|
668
|
+
"""
|
|
669
|
+
Prints out the summary after processing has ended with the info of what was done and what failed.
|
|
670
|
+
|
|
671
|
+
:param effective_groups: list of effective groups that the run was done on
|
|
672
|
+
:param effective_projects: list of effective projects that the run was done on
|
|
673
|
+
:param successful_groups: number of successfully processed groups
|
|
674
|
+
:param successful_projects: number of successfully processed projects
|
|
675
|
+
:param failed_groups: a dict with failed groups, where keys are their numbers in the processing order
|
|
676
|
+
:param failed_projects: a dict with failed projects, where keys are their numbers in the processing order
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
if len(effective_groups) > 0 or len(effective_projects) > 0:
|
|
680
|
+
info(f"# of groups processed successfully: {successful_groups}")
|
|
681
|
+
info(f"# of projects processed successfully: {successful_projects}")
|
|
682
|
+
|
|
683
|
+
if len(failed_groups) > 0:
|
|
684
|
+
console.print(f"# of groups failed: {len(failed_groups)}", style="red")
|
|
685
|
+
for group_number in failed_groups.keys():
|
|
686
|
+
console.print(f"Failed group {group_number}: {failed_groups[group_number]}", style="red")
|
|
687
|
+
if len(failed_projects) > 0:
|
|
688
|
+
console.print(f"# of projects failed: {len(failed_projects)}", style="red")
|
|
689
|
+
for project_number in failed_projects.keys():
|
|
690
|
+
console.print(f"Failed project {project_number}: {failed_projects[project_number]}", style="red")
|
|
691
|
+
|
|
692
|
+
if len(failed_groups) > 0 or len(failed_projects) > 0:
|
|
693
|
+
sys.exit(EXIT_PROCESSING_ERROR)
|
|
694
|
+
elif successful_groups > 0 or successful_projects > 0:
|
|
695
|
+
console.print("All requested groups/projects processed successfully! :sparkles:", style="green")
|
|
696
|
+
else:
|
|
697
|
+
console.print("Nothing to do.", style="yellow")
|
|
698
|
+
|
|
699
|
+
@classmethod
|
|
700
|
+
def _info_group_count(cls, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
701
|
+
cls._info_count("purple", prefix, i, n, second_color, second_text)
|
|
702
|
+
|
|
703
|
+
@classmethod
|
|
704
|
+
def _info_project_count(cls, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
705
|
+
cls._info_count("green", prefix, i, n, second_color, second_text)
|
|
706
|
+
|
|
707
|
+
@classmethod
|
|
708
|
+
def _info_count(cls, color: str, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
709
|
+
num_digits = len(str(n))
|
|
710
|
+
counter_format = f"(%{num_digits}d/%d)"
|
|
711
|
+
counter_str = counter_format % (i, n)
|
|
712
|
+
console.print(f"[bold {color}]{prefix} {counter_str}[/] [{second_color}]{second_text}[/]")
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class Formatter(
|
|
716
|
+
argparse.ArgumentDefaultsHelpFormatter,
|
|
717
|
+
argparse.RawTextHelpFormatter,
|
|
718
|
+
):
|
|
719
|
+
pass
|