gitlabform 5.1.1__tar.gz → 5.3.0__tar.gz
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-5.1.1 → gitlabform-5.3.0}/PKG-INFO +7 -7
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/__init__.py +85 -114
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/core.py +23 -28
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/transform.py +10 -10
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/__init__.py +1 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/core.py +3 -4
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/project_protected_environments.py +2 -2
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/python_gitlab.py +2 -2
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/lists/__init__.py +1 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/lists/filter.py +4 -5
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/lists/groups.py +4 -5
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/lists/projects.py +52 -24
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/output.py +7 -12
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/__init__.py +3 -4
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/abstract_processor.py +9 -9
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_members_processor.py +19 -18
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_saml_links_processor.py +1 -2
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_variables_processor.py +2 -2
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/multiple_entities_processor.py +17 -18
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/branches_processor.py +25 -29
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/files_processor.py +15 -17
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/integrations_processor.py +5 -5
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/job_token_scope_processor.py +1 -1
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/members_processor.py +22 -22
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/merge_requests_approvals.py +11 -8
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_processor.py +7 -7
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_security_settings.py +1 -3
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_variables_processor.py +7 -8
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/remote_mirrors_processor.py +14 -16
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/resource_groups_processor.py +2 -4
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/tags_processor.py +6 -8
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/util/difference_logger.py +2 -2
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/util/labels_processor.py +4 -5
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/util/variables_processor.py +8 -9
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/PKG-INFO +7 -7
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/requires.txt +6 -6
- {gitlabform-5.1.1 → gitlabform-5.3.0}/pyproject.toml +13 -12
- {gitlabform-5.1.1 → gitlabform-5.3.0}/LICENSE +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/README.md +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/common.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/groups.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/configuration/projects.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/constants.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/commits.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/group_badges.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/group_ldap_links.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/groups.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/merge_requests.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/pipelines.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/project_badges.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/project_deploy_keys.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/projects.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/gitlab/variables.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/application/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/defining_keys.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/group/group_settings_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/badges_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/hooks_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/project_settings_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/project/schedules_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/shared/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/util/__init__.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/processors/util/decorators.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/run.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform/util.py +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/SOURCES.txt +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/dependency_links.txt +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/entry_points.txt +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/gitlabform.egg-info/top_level.txt +0 -0
- {gitlabform-5.1.1 → gitlabform-5.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabform
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
|
|
5
5
|
Author: Greg Dubicki and Contributors
|
|
6
6
|
Project-URL: Homepage, https://gitlabform.github.io/gitlabform/
|
|
@@ -21,8 +21,6 @@ Requires-Python: >=3.12.0
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: certifi==2026.2.25
|
|
24
|
-
Requires-Dist: cli-ui==0.19.0
|
|
25
|
-
Requires-Dist: ez-yaml==1.2.0
|
|
26
24
|
Requires-Dist: Jinja2==3.1.6
|
|
27
25
|
Requires-Dist: luddite==1.0.4
|
|
28
26
|
Requires-Dist: MarkupSafe==3.0.3
|
|
@@ -31,19 +29,21 @@ Requires-Dist: packaging==26.0
|
|
|
31
29
|
Requires-Dist: python-gitlab==8.2.0
|
|
32
30
|
Requires-Dist: python-gitlab[graphql]==8.2.0
|
|
33
31
|
Requires-Dist: requests==2.33.1
|
|
32
|
+
Requires-Dist: rich==15.0.0
|
|
34
33
|
Requires-Dist: ruamel.yaml==0.17.21
|
|
35
34
|
Requires-Dist: yamlpath==3.8.2
|
|
35
|
+
Requires-Dist: ez-yaml==1.2.0
|
|
36
36
|
Provides-Extra: test
|
|
37
37
|
Requires-Dist: coverage==7.13.5; extra == "test"
|
|
38
|
-
Requires-Dist: cryptography==46.0.
|
|
38
|
+
Requires-Dist: cryptography==46.0.7; extra == "test"
|
|
39
39
|
Requires-Dist: deepdiff==9.0.0; extra == "test"
|
|
40
|
-
Requires-Dist: mypy==1.20.
|
|
40
|
+
Requires-Dist: mypy==1.20.1; extra == "test"
|
|
41
41
|
Requires-Dist: mypy-extensions==1.1.0; extra == "test"
|
|
42
42
|
Requires-Dist: pre-commit==2.21.0; extra == "test"
|
|
43
|
-
Requires-Dist: pytest==9.0.
|
|
43
|
+
Requires-Dist: pytest==9.0.3; extra == "test"
|
|
44
44
|
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
45
45
|
Requires-Dist: pytest-rerunfailures==16.1; extra == "test"
|
|
46
|
-
Requires-Dist: types-requests==2.33.0.
|
|
46
|
+
Requires-Dist: types-requests==2.33.0.20260408; extra == "test"
|
|
47
47
|
Requires-Dist: types-setuptools==82.0.0.20260402; extra == "test"
|
|
48
48
|
Requires-Dist: xkcdpass==1.30.0; extra == "test"
|
|
49
49
|
Provides-Extra: docs
|
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from logging import debug
|
|
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
|
|
3
7
|
|
|
4
8
|
import argparse
|
|
5
|
-
import cli_ui
|
|
6
9
|
import logging
|
|
7
10
|
import luddite
|
|
8
11
|
from importlib.metadata import version as package_version
|
|
9
12
|
import textwrap
|
|
10
13
|
import traceback
|
|
11
|
-
|
|
12
|
-
Symbol,
|
|
13
|
-
reset,
|
|
14
|
-
blue,
|
|
15
|
-
message,
|
|
16
|
-
error,
|
|
17
|
-
info,
|
|
18
|
-
fatal,
|
|
19
|
-
info_1,
|
|
20
|
-
debug as verbose,
|
|
21
|
-
red,
|
|
22
|
-
green,
|
|
23
|
-
yellow,
|
|
24
|
-
Token,
|
|
25
|
-
purple,
|
|
26
|
-
warning,
|
|
27
|
-
)
|
|
14
|
+
|
|
28
15
|
from packaging import version
|
|
29
16
|
from typing import Any, Tuple
|
|
30
17
|
|
|
@@ -46,11 +33,14 @@ from gitlabform.processors.application import ApplicationProcessors
|
|
|
46
33
|
from gitlabform.processors.group import GroupProcessors
|
|
47
34
|
from gitlabform.processors.project import ProjectProcessors
|
|
48
35
|
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
49
38
|
|
|
50
39
|
class GitLabForm:
|
|
51
40
|
def __init__(
|
|
52
41
|
self,
|
|
53
42
|
include_archived_projects=True,
|
|
43
|
+
include_projects_scheduled_for_deletion=True,
|
|
54
44
|
target=None,
|
|
55
45
|
config_string=None,
|
|
56
46
|
noop=False,
|
|
@@ -72,6 +62,7 @@ class GitLabForm:
|
|
|
72
62
|
self.output_file = output_file
|
|
73
63
|
self.skip_version_check = True
|
|
74
64
|
self.include_archived_projects = include_archived_projects
|
|
65
|
+
self.include_projects_scheduled_for_deletion = include_projects_scheduled_for_deletion
|
|
75
66
|
self.just_show_version = False
|
|
76
67
|
self.terminate_after_error = True
|
|
77
68
|
self.only_sections = "all"
|
|
@@ -95,6 +86,7 @@ class GitLabForm:
|
|
|
95
86
|
self.output_file,
|
|
96
87
|
self.skip_version_check,
|
|
97
88
|
self.include_archived_projects,
|
|
89
|
+
self.include_projects_scheduled_for_deletion,
|
|
98
90
|
self.just_show_version,
|
|
99
91
|
self.terminate_after_error,
|
|
100
92
|
self.only_sections,
|
|
@@ -109,10 +101,8 @@ class GitLabForm:
|
|
|
109
101
|
sys.exit(0)
|
|
110
102
|
|
|
111
103
|
if not self.target:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
115
|
-
)
|
|
104
|
+
critical("target parameter is required.")
|
|
105
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
116
106
|
|
|
117
107
|
self.gitlab, self.configuration = self._initialize_configuration_and_gitlab()
|
|
118
108
|
|
|
@@ -128,6 +118,7 @@ class GitLabForm:
|
|
|
128
118
|
self.gitlab,
|
|
129
119
|
self.configuration,
|
|
130
120
|
self.include_archived_projects,
|
|
121
|
+
self.include_projects_scheduled_for_deletion,
|
|
131
122
|
self.recurse_subgroups,
|
|
132
123
|
)
|
|
133
124
|
|
|
@@ -236,6 +227,13 @@ class GitLabForm:
|
|
|
236
227
|
help="Includes processing projects that are archived",
|
|
237
228
|
)
|
|
238
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
|
+
|
|
239
237
|
parser.add_argument(
|
|
240
238
|
"-t",
|
|
241
239
|
"--terminate",
|
|
@@ -317,6 +315,7 @@ class GitLabForm:
|
|
|
317
315
|
args.output_file,
|
|
318
316
|
args.skip_version_check,
|
|
319
317
|
args.include_archived_projects,
|
|
318
|
+
args.include_projects_scheduled_for_deletion,
|
|
320
319
|
args.just_show_version,
|
|
321
320
|
args.terminate_after_error,
|
|
322
321
|
args.only_sections,
|
|
@@ -326,37 +325,26 @@ class GitLabForm:
|
|
|
326
325
|
|
|
327
326
|
def _configure_output(self, tests=False) -> None:
|
|
328
327
|
"""
|
|
329
|
-
Configures the application output using
|
|
328
|
+
Configures the application output using logging, based on debug and verbose flags:
|
|
330
329
|
|
|
331
|
-
* normal mode -
|
|
332
|
-
* verbose mode -
|
|
333
|
-
* debug / tests mode -
|
|
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)
|
|
334
333
|
|
|
335
334
|
:param tests: True if we are running in tests mode
|
|
336
335
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if self.debug or tests:
|
|
341
|
-
# debug / tests
|
|
342
|
-
cli_ui_verbose = True
|
|
336
|
+
rich_tracebacks = False
|
|
337
|
+
if tests or self.debug:
|
|
343
338
|
level = logging.DEBUG
|
|
339
|
+
rich_tracebacks = True
|
|
344
340
|
elif self.verbose:
|
|
345
|
-
|
|
346
|
-
cli_ui_verbose = True
|
|
347
|
-
# de facto disabled as we don't use logging different from debug in this project
|
|
348
|
-
level = logging.FATAL
|
|
341
|
+
level = logging.INFO
|
|
349
342
|
else:
|
|
350
|
-
|
|
351
|
-
cli_ui_verbose = False
|
|
352
|
-
# de facto disabled as we don't use logging different from debug in this project
|
|
353
|
-
level = logging.FATAL
|
|
354
|
-
|
|
355
|
-
cli_ui.setup(verbose=cli_ui_verbose)
|
|
356
|
-
logging.getLogger().setLevel(level)
|
|
343
|
+
level = logging.WARNING
|
|
357
344
|
|
|
358
|
-
|
|
359
|
-
|
|
345
|
+
logging.basicConfig(
|
|
346
|
+
level=level, format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=rich_tracebacks)]
|
|
347
|
+
)
|
|
360
348
|
|
|
361
349
|
def _initialize_configuration_and_gitlab(self) -> Tuple[GitLab, Configuration]:
|
|
362
350
|
"""
|
|
@@ -378,20 +366,14 @@ class GitLabForm:
|
|
|
378
366
|
configuration_transformers.transform(configuration)
|
|
379
367
|
|
|
380
368
|
except ConfigFileNotFoundException as e:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
384
|
-
)
|
|
369
|
+
critical(f"Config file not found at: {e}")
|
|
370
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
385
371
|
except ConfigInvalidException as e:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
389
|
-
)
|
|
372
|
+
critical(f"Invalid config:\n{e.underlying}")
|
|
373
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
390
374
|
except TestRequestFailedException as e:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
exit_code=EXIT_PROCESSING_ERROR,
|
|
394
|
-
)
|
|
375
|
+
critical(f"GitLab test request failed:\n{e.underlying}")
|
|
376
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
395
377
|
|
|
396
378
|
return gitlab, configuration
|
|
397
379
|
|
|
@@ -430,9 +412,8 @@ class GitLabForm:
|
|
|
430
412
|
"@",
|
|
431
413
|
group_number,
|
|
432
414
|
len(groups),
|
|
433
|
-
yellow,
|
|
415
|
+
"yellow",
|
|
434
416
|
f"Skipping group {group} as requested to start from {self.start_from_group}...",
|
|
435
|
-
reset,
|
|
436
417
|
)
|
|
437
418
|
continue
|
|
438
419
|
|
|
@@ -444,6 +425,7 @@ class GitLabForm:
|
|
|
444
425
|
"@",
|
|
445
426
|
group_number,
|
|
446
427
|
len(groups),
|
|
428
|
+
"black",
|
|
447
429
|
f"Processing group: {group}",
|
|
448
430
|
)
|
|
449
431
|
|
|
@@ -489,9 +471,8 @@ class GitLabForm:
|
|
|
489
471
|
"*",
|
|
490
472
|
project_number,
|
|
491
473
|
len(projects),
|
|
492
|
-
yellow,
|
|
474
|
+
"yellow",
|
|
493
475
|
f"Skipping project {project_and_group} as requested to start from {self.start_from}...",
|
|
494
|
-
reset,
|
|
495
476
|
)
|
|
496
477
|
continue
|
|
497
478
|
|
|
@@ -503,6 +484,7 @@ class GitLabForm:
|
|
|
503
484
|
"*",
|
|
504
485
|
project_number,
|
|
505
486
|
len(projects),
|
|
487
|
+
"black",
|
|
506
488
|
f"Processing project: {project_and_group}",
|
|
507
489
|
)
|
|
508
490
|
|
|
@@ -564,15 +546,15 @@ class GitLabForm:
|
|
|
564
546
|
|
|
565
547
|
local_version = package_version("gitlabform")
|
|
566
548
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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}[/]"
|
|
572
554
|
|
|
573
555
|
if skip_version_check:
|
|
574
|
-
# just print
|
|
575
|
-
print()
|
|
556
|
+
# just print version in use
|
|
557
|
+
console.print(version_text)
|
|
576
558
|
else:
|
|
577
559
|
try:
|
|
578
560
|
latest_version = luddite.get_version_pypi("gitlabform")
|
|
@@ -582,24 +564,26 @@ class GitLabForm:
|
|
|
582
564
|
error(f"Checking latest version failed:\n{e}")
|
|
583
565
|
return
|
|
584
566
|
|
|
567
|
+
latest_stable_text = "(the latest stable is {latest_version})"
|
|
568
|
+
|
|
585
569
|
if local_version == latest_version:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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} ☺️"
|
|
590
574
|
elif version.parse(local_version) < version.parse(latest_version):
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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}"
|
|
595
579
|
else:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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}"
|
|
600
584
|
|
|
601
585
|
# complete the line with a line ending
|
|
602
|
-
|
|
586
|
+
console.print(f"{version_text} {version_info}")
|
|
603
587
|
|
|
604
588
|
def _get_groups_and_projects(
|
|
605
589
|
self,
|
|
@@ -633,10 +617,8 @@ class GitLabForm:
|
|
|
633
617
|
error_message = "Configuration does not have any groups or projects defined!"
|
|
634
618
|
else:
|
|
635
619
|
error_message = f"Project or group {target} cannot be found in GitLab!"
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
639
|
-
)
|
|
620
|
+
critical(error_message)
|
|
621
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
640
622
|
|
|
641
623
|
self.groups_and_projects_filters.filter(groups, projects)
|
|
642
624
|
|
|
@@ -652,7 +634,7 @@ class GitLabForm:
|
|
|
652
634
|
|
|
653
635
|
:param entities: groups or projects
|
|
654
636
|
"""
|
|
655
|
-
|
|
637
|
+
info(f"# of {entities.name} to process: {len(entities.get_effective())}")
|
|
656
638
|
|
|
657
639
|
entities_omitted = ""
|
|
658
640
|
entities_verbose = f"{entities.name}: {entities.get_effective()}"
|
|
@@ -669,9 +651,9 @@ class GitLabForm:
|
|
|
669
651
|
entities_omitted += ")"
|
|
670
652
|
|
|
671
653
|
if entities_omitted:
|
|
672
|
-
|
|
654
|
+
info(entities_omitted)
|
|
673
655
|
|
|
674
|
-
|
|
656
|
+
info(entities_verbose)
|
|
675
657
|
|
|
676
658
|
@classmethod
|
|
677
659
|
def _show_summary(
|
|
@@ -695,50 +677,39 @@ class GitLabForm:
|
|
|
695
677
|
"""
|
|
696
678
|
|
|
697
679
|
if len(effective_groups) > 0 or len(effective_projects) > 0:
|
|
698
|
-
|
|
699
|
-
|
|
680
|
+
info(f"# of groups processed successfully: {successful_groups}")
|
|
681
|
+
info(f"# of projects processed successfully: {successful_projects}")
|
|
700
682
|
|
|
701
683
|
if len(failed_groups) > 0:
|
|
702
|
-
|
|
684
|
+
console.print(f"# of groups failed: {len(failed_groups)}", style="red")
|
|
703
685
|
for group_number in failed_groups.keys():
|
|
704
|
-
|
|
705
|
-
info_1(red, f"Failed group {group_number}: {failed_groups[group_number]}", reset)
|
|
706
|
-
# fmt: on
|
|
686
|
+
console.print(f"Failed group {group_number}: {failed_groups[group_number]}", style="red")
|
|
707
687
|
if len(failed_projects) > 0:
|
|
708
|
-
#
|
|
709
|
-
info_1(red, f"# of projects failed: {len(failed_projects)}", reset)
|
|
710
|
-
# fmt: on
|
|
688
|
+
console.print(f"# of projects failed: {len(failed_projects)}", style="red")
|
|
711
689
|
for project_number in failed_projects.keys():
|
|
712
|
-
|
|
713
|
-
info_1(red, f"Failed project {project_number}: {failed_projects[project_number]}", reset)
|
|
714
|
-
# fmt: on
|
|
690
|
+
console.print(f"Failed project {project_number}: {failed_projects[project_number]}", style="red")
|
|
715
691
|
|
|
716
692
|
if len(failed_groups) > 0 or len(failed_projects) > 0:
|
|
717
693
|
sys.exit(EXIT_PROCESSING_ERROR)
|
|
718
694
|
elif successful_groups > 0 or successful_projects > 0:
|
|
719
|
-
|
|
720
|
-
shine = Symbol("✨", "!!!")
|
|
721
|
-
info_1(green, "All requested groups/projects processed successfully!", reset, shine)
|
|
722
|
-
# fmt: on
|
|
695
|
+
console.print("All requested groups/projects processed successfully! :sparkles:", style="green")
|
|
723
696
|
else:
|
|
724
|
-
|
|
725
|
-
info_1(yellow, "Nothing to do.", reset)
|
|
726
|
-
# fmt: on
|
|
697
|
+
console.print("Nothing to do.", style="yellow")
|
|
727
698
|
|
|
728
699
|
@classmethod
|
|
729
|
-
def _info_group_count(cls, prefix, i: int, n: int,
|
|
730
|
-
cls._info_count(purple, prefix, i, n,
|
|
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)
|
|
731
702
|
|
|
732
703
|
@classmethod
|
|
733
|
-
def _info_project_count(cls, prefix, i: int, n: int,
|
|
734
|
-
cls._info_count(green, prefix, i, n,
|
|
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)
|
|
735
706
|
|
|
736
707
|
@classmethod
|
|
737
|
-
def _info_count(cls, color, prefix, i: int, n: int,
|
|
708
|
+
def _info_count(cls, color: str, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
738
709
|
num_digits = len(str(n))
|
|
739
710
|
counter_format = f"(%{num_digits}d/%d)"
|
|
740
711
|
counter_str = counter_format % (i, n)
|
|
741
|
-
|
|
712
|
+
console.print(f"[bold {color}]{prefix} {counter_str}[/] [{second_color}]{second_text}[/]")
|
|
742
713
|
|
|
743
714
|
|
|
744
715
|
class Formatter(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
import os
|
|
@@ -10,8 +11,8 @@ from pathlib import Path
|
|
|
10
11
|
from ruamel.yaml.scalarstring import ScalarString
|
|
11
12
|
from types import SimpleNamespace
|
|
12
13
|
|
|
13
|
-
from
|
|
14
|
-
|
|
14
|
+
from logging import critical, info
|
|
15
|
+
|
|
15
16
|
from mergedeep import merge
|
|
16
17
|
from yamlpath.common import Parsers
|
|
17
18
|
from yamlpath.wrappers import ConsolePrinter
|
|
@@ -28,10 +29,8 @@ class ConfigurationCore(ABC):
|
|
|
28
29
|
|
|
29
30
|
def __init__(self, config_path=None, config_string=None):
|
|
30
31
|
if config_path and config_string:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
34
|
-
)
|
|
32
|
+
critical("Please initialize with either config_path or config_string, not both.")
|
|
33
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
35
34
|
try:
|
|
36
35
|
if config_string:
|
|
37
36
|
self.config = self._parse_yaml(config_string, config_string=True)
|
|
@@ -44,22 +43,22 @@ class ConfigurationCore(ABC):
|
|
|
44
43
|
# below checks are only needed in the non-test mode, when the config is read from file
|
|
45
44
|
|
|
46
45
|
if self.config.get("example_config"):
|
|
47
|
-
|
|
46
|
+
critical(
|
|
48
47
|
"Example config detected, aborting.\n"
|
|
49
48
|
"Haven't you forgotten to use `-c <config_file>` parameter?\n"
|
|
50
49
|
"If you created your config based on the example config.yml,"
|
|
51
|
-
" then please remove 'example_config' key."
|
|
52
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
50
|
+
" then please remove 'example_config' key."
|
|
53
51
|
)
|
|
52
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
54
53
|
|
|
55
54
|
if self.config.get("config_version", 1) != 4:
|
|
56
|
-
|
|
55
|
+
critical(
|
|
57
56
|
"This version of GitLabForm requires 'config_version: 4' entry in the config. "
|
|
58
57
|
"This ensures that if the application behavior changes in a backward-incompatible way,"
|
|
59
58
|
" you won't apply unwanted configuration to your GitLab instance.\n"
|
|
60
|
-
"Please follow this guide: https://gitlabform.github.io/gitlabform/upgrade/\n"
|
|
61
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
59
|
+
"Please follow this guide: https://gitlabform.github.io/gitlabform/upgrade/\n"
|
|
62
60
|
)
|
|
61
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
63
62
|
|
|
64
63
|
self._find_almost_duplicates()
|
|
65
64
|
|
|
@@ -100,11 +99,11 @@ class ConfigurationCore(ABC):
|
|
|
100
99
|
|
|
101
100
|
if config_string:
|
|
102
101
|
config = textwrap.dedent(source)
|
|
103
|
-
|
|
102
|
+
info("Reading config from the provided string.")
|
|
104
103
|
yaml_data, doc_loaded = Parsers.get_yaml_data(yaml, log, config, literal=True)
|
|
105
104
|
else:
|
|
106
105
|
config_path = source
|
|
107
|
-
|
|
106
|
+
info(f"Reading config from file: {config_path}")
|
|
108
107
|
yaml_data, doc_loaded = Parsers.get_yaml_data(yaml, log, config_path)
|
|
109
108
|
|
|
110
109
|
if doc_loaded:
|
|
@@ -159,11 +158,11 @@ class ConfigurationCore(ABC):
|
|
|
159
158
|
for key, value in config.items():
|
|
160
159
|
if "inherit" == key:
|
|
161
160
|
parent_key_description = ' under key "' + parent_key + '"' if parent_key else ""
|
|
162
|
-
|
|
161
|
+
critical(
|
|
163
162
|
f'The inheritance-break flag set in "{section_name}"{parent_key_description} is invalid\n'
|
|
164
|
-
f"because it has no higher level setting to inherit from.\n"
|
|
165
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
163
|
+
f"because it has no higher level setting to inherit from.\n"
|
|
166
164
|
)
|
|
165
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
167
166
|
elif type(value) in [CommentedMap, dict]:
|
|
168
167
|
ConfigurationCore._validate_break_inheritance_flag(value, section_name, key)
|
|
169
168
|
|
|
@@ -194,10 +193,8 @@ class ConfigurationCore(ABC):
|
|
|
194
193
|
replace_config_section(merged_dict, parent_path, specific_config)
|
|
195
194
|
break
|
|
196
195
|
elif value:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
200
|
-
)
|
|
196
|
+
critical(f"Cannot set the inheritance break flag with true\n")
|
|
197
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
201
198
|
elif type(value) in [CommentedMap, dict]:
|
|
202
199
|
break_inheritance(value, parent_path + (key,))
|
|
203
200
|
|
|
@@ -262,12 +259,12 @@ class ConfigurationCore(ABC):
|
|
|
262
259
|
if self.get(path, 0):
|
|
263
260
|
almost_duplicates = self._find_almost_duplicates_in(path)
|
|
264
261
|
if almost_duplicates:
|
|
265
|
-
|
|
262
|
+
critical(
|
|
266
263
|
f"There are almost duplicates in the keys of {path} - they differ only in case.\n"
|
|
267
264
|
f"They are: {', '.join(almost_duplicates)}\n"
|
|
268
|
-
f"This is not allowed as we ignore the case for group and project names."
|
|
269
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
265
|
+
f"This is not allowed as we ignore the case for group and project names."
|
|
270
266
|
)
|
|
267
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
271
268
|
|
|
272
269
|
def _find_almost_duplicates_in(self, configuration_path):
|
|
273
270
|
"""
|
|
@@ -322,7 +319,5 @@ class KeyNotFoundException(Exception):
|
|
|
322
319
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
323
320
|
self.key = key
|
|
324
321
|
else:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
328
|
-
)
|
|
322
|
+
critical(f"Unable to find the key: {key.replace('|', '.')}\n")
|
|
323
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import sys
|
|
2
|
+
from logging import debug, info, critical
|
|
2
3
|
from abc import ABC, abstractmethod
|
|
3
4
|
from ez_yaml import ez_yaml
|
|
4
5
|
from ruamel.yaml import YAML
|
|
5
6
|
from types import SimpleNamespace
|
|
6
7
|
|
|
7
|
-
from cli_ui import fatal, warning, debug as verbose
|
|
8
8
|
from ruamel.yaml.comments import CommentedMap
|
|
9
9
|
from yamlpath import Processor
|
|
10
10
|
from yamlpath.exceptions import YAMLPathException
|
|
@@ -72,7 +72,7 @@ class UserTransformer(ConfigurationTransformer):
|
|
|
72
72
|
logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False)
|
|
73
73
|
log = ConsolePrinter(logging_args)
|
|
74
74
|
processor = Processor(log, configuration.config)
|
|
75
|
-
|
|
75
|
+
info("Getting user ids for users defined in protect_environments config")
|
|
76
76
|
try:
|
|
77
77
|
for node_coordinate in processor.get_nodes(
|
|
78
78
|
"projects_and_groups.*.protected_environments.*.deploy_access_levels.user",
|
|
@@ -86,7 +86,7 @@ class UserTransformer(ConfigurationTransformer):
|
|
|
86
86
|
# under the given path
|
|
87
87
|
pass
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
info("Getting user ids for users defined in merge_requests_approval_rules config")
|
|
90
90
|
try:
|
|
91
91
|
for node_coordinate in processor.get_nodes(
|
|
92
92
|
"**.merge_requests_approval_rules.*.users",
|
|
@@ -218,11 +218,11 @@ class AccessLevelsTransformer(ConfigurationTransformer):
|
|
|
218
218
|
access_level_string = str(node_coordinate.node)
|
|
219
219
|
node_coordinate.parent[node_coordinate.parentref] = AccessLevel.get_value(access_level_string)
|
|
220
220
|
except KeyError:
|
|
221
|
-
|
|
221
|
+
critical(
|
|
222
222
|
f"Configuration string '{access_level_string}' is not one of the valid access levels:"
|
|
223
|
-
f" {', '.join(AccessLevel.get_canonical_names())}"
|
|
224
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
223
|
+
f" {', '.join(AccessLevel.get_canonical_names())}"
|
|
225
224
|
)
|
|
225
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
226
226
|
except YAMLPathException:
|
|
227
227
|
# this just means that we haven't found any keys in YAML
|
|
228
228
|
# under the given path
|
|
@@ -248,11 +248,11 @@ class AccessLevelsTransformer(ConfigurationTransformer):
|
|
|
248
248
|
access_level_string
|
|
249
249
|
)
|
|
250
250
|
except KeyError:
|
|
251
|
-
|
|
251
|
+
critical(
|
|
252
252
|
f"Configuration string '{access_level_string}' is not one of the valid access levels:"
|
|
253
|
-
f" {', '.join(AccessLevel.get_canonical_names())}"
|
|
254
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
253
|
+
f" {', '.join(AccessLevel.get_canonical_names())}"
|
|
255
254
|
)
|
|
255
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
256
256
|
except YAMLPathException:
|
|
257
257
|
# this just means that we haven't found any keys in YAML
|
|
258
258
|
# under the given path
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
-
from logging import debug
|
|
4
|
+
from logging import debug, info, warning
|
|
5
5
|
from typing import Union
|
|
6
6
|
from urllib import parse
|
|
7
7
|
|
|
@@ -12,7 +12,6 @@ import requests
|
|
|
12
12
|
|
|
13
13
|
# noinspection PyPackageRequirements
|
|
14
14
|
import urllib3
|
|
15
|
-
from cli_ui import debug as verbose, warning
|
|
16
15
|
from requests.adapters import HTTPAdapter
|
|
17
16
|
|
|
18
17
|
# noinspection PyPackageRequirements
|
|
@@ -73,7 +72,7 @@ class GitLabCore:
|
|
|
73
72
|
|
|
74
73
|
try:
|
|
75
74
|
version_response = self._make_requests_to_api("version")
|
|
76
|
-
|
|
75
|
+
info(
|
|
77
76
|
f"Connected to GitLab version: {version_response['version']} ({version_response['revision']}), Enterprise Edition: {version_response['enterprise']}"
|
|
78
77
|
)
|
|
79
78
|
self.version = version_response["version"]
|
|
@@ -89,7 +88,7 @@ class GitLabCore:
|
|
|
89
88
|
self.admin = True
|
|
90
89
|
else:
|
|
91
90
|
self.admin = False
|
|
92
|
-
|
|
91
|
+
info(f"Connected as: {current_user['username']}, admin: {'yes' if self.admin else 'no'}")
|
|
93
92
|
if not self.admin:
|
|
94
93
|
warning("Connected as non-admin. You may encounter permission issues.")
|
|
95
94
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from logging import info
|
|
2
2
|
|
|
3
3
|
from gitlabform.gitlab.projects import GitLabProjects
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ class GitLabProjectProtectedEnvironments(GitLabProjects):
|
|
|
20
20
|
|
|
21
21
|
# TODO: remove this when this issue is resolved -> https://gitlab.com/gitlab-org/gitlab/-/issues/378657
|
|
22
22
|
if retry and (len(protected_env_cfg["deploy_access_levels"]) != len(response["deploy_access_levels"])):
|
|
23
|
-
|
|
23
|
+
info(f'Gitlab\'s returned "deploy_access_levels" differs from the sent cfg, trying again...')
|
|
24
24
|
|
|
25
25
|
self.unprotect_environment(project_and_group_name, protected_env_cfg)
|
|
26
26
|
|