gitlabform 5.1.0__tar.gz → 5.2.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.0 → gitlabform-5.2.0}/PKG-INFO +9 -9
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/__init__.py +74 -114
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/core.py +47 -40
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/transform.py +10 -10
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/core.py +3 -4
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/project_protected_environments.py +2 -2
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/python_gitlab.py +2 -2
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/lists/filter.py +4 -5
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/lists/groups.py +4 -5
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/lists/projects.py +11 -10
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/output.py +7 -12
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/__init__.py +3 -4
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/abstract_processor.py +9 -9
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_members_processor.py +19 -18
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_saml_links_processor.py +1 -2
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_variables_processor.py +2 -2
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/multiple_entities_processor.py +17 -18
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/branches_processor.py +197 -149
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/files_processor.py +15 -17
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/integrations_processor.py +5 -5
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/job_token_scope_processor.py +1 -1
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/members_processor.py +22 -22
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/merge_requests_approvals.py +11 -8
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_processor.py +7 -7
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_security_settings.py +1 -3
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_variables_processor.py +7 -8
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/remote_mirrors_processor.py +14 -16
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/resource_groups_processor.py +2 -4
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/tags_processor.py +6 -8
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/util/difference_logger.py +2 -2
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/util/labels_processor.py +4 -5
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/util/variables_processor.py +8 -9
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/PKG-INFO +9 -9
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/requires.txt +8 -8
- {gitlabform-5.1.0 → gitlabform-5.2.0}/pyproject.toml +13 -13
- {gitlabform-5.1.0 → gitlabform-5.2.0}/LICENSE +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/README.md +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/common.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/groups.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/configuration/projects.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/constants.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/commits.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/group_badges.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/group_ldap_links.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/groups.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/merge_requests.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/pipelines.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/project_badges.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/project_deploy_keys.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/projects.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/gitlab/variables.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/lists/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/application/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/application/application_settings_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/defining_keys.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_badges_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_hooks_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_labels_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_push_rules_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/group/group_settings_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/badges_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/hooks_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_labels_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/project_settings_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/project/schedules_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/shared/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/util/__init__.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/processors/util/decorators.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/run.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform/util.py +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/SOURCES.txt +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/dependency_links.txt +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/entry_points.txt +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.0}/gitlabform.egg-info/top_level.txt +0 -0
- {gitlabform-5.1.0 → gitlabform-5.2.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.2.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
|
|
@@ -30,21 +28,23 @@ Requires-Dist: mergedeep==1.3.4
|
|
|
30
28
|
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
|
-
Requires-Dist: requests==2.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.
|
|
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.
|
|
47
|
-
Requires-Dist: types-setuptools==82.0.0.
|
|
46
|
+
Requires-Dist: types-requests==2.33.0.20260408; extra == "test"
|
|
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
|
|
50
50
|
Requires-Dist: mkdocs; 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,6 +33,8 @@ 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__(
|
|
@@ -109,10 +98,8 @@ class GitLabForm:
|
|
|
109
98
|
sys.exit(0)
|
|
110
99
|
|
|
111
100
|
if not self.target:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
115
|
-
)
|
|
101
|
+
critical("target parameter is required.")
|
|
102
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
116
103
|
|
|
117
104
|
self.gitlab, self.configuration = self._initialize_configuration_and_gitlab()
|
|
118
105
|
|
|
@@ -326,37 +313,27 @@ class GitLabForm:
|
|
|
326
313
|
|
|
327
314
|
def _configure_output(self, tests=False) -> None:
|
|
328
315
|
"""
|
|
329
|
-
Configures the application output using
|
|
316
|
+
Configures the application output using logging, based on debug and verbose flags:
|
|
330
317
|
|
|
331
|
-
* normal mode -
|
|
332
|
-
* verbose mode -
|
|
333
|
-
* debug / tests mode -
|
|
318
|
+
* normal mode - logging only WARNING logs
|
|
319
|
+
* verbose mode - logging INFO level and above
|
|
320
|
+
* debug / tests mode - logging DEBUG level and above, along with rich exception tracebacks (may expose secrets)
|
|
334
321
|
|
|
335
322
|
:param tests: True if we are running in tests mode
|
|
336
323
|
"""
|
|
337
|
-
|
|
338
|
-
logging.basicConfig()
|
|
339
|
-
|
|
340
|
-
if self.debug or tests:
|
|
341
|
-
# debug / tests
|
|
342
|
-
cli_ui_verbose = True
|
|
324
|
+
if tests or self.debug:
|
|
343
325
|
level = logging.DEBUG
|
|
326
|
+
rich_tracebacks = True
|
|
344
327
|
elif self.verbose:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# de facto disabled as we don't use logging different from debug in this project
|
|
348
|
-
level = logging.FATAL
|
|
328
|
+
level = logging.INFO
|
|
329
|
+
rich_tracebacks = False
|
|
349
330
|
else:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
# de facto disabled as we don't use logging different from debug in this project
|
|
353
|
-
level = logging.FATAL
|
|
331
|
+
level = logging.WARNING
|
|
332
|
+
rich_tracebacks = False
|
|
354
333
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
fmt = logging.Formatter("%(message)s")
|
|
359
|
-
logging.getLogger().handlers[0].setFormatter(fmt)
|
|
334
|
+
logging.basicConfig(
|
|
335
|
+
level=level, format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=rich_tracebacks)]
|
|
336
|
+
)
|
|
360
337
|
|
|
361
338
|
def _initialize_configuration_and_gitlab(self) -> Tuple[GitLab, Configuration]:
|
|
362
339
|
"""
|
|
@@ -378,20 +355,14 @@ class GitLabForm:
|
|
|
378
355
|
configuration_transformers.transform(configuration)
|
|
379
356
|
|
|
380
357
|
except ConfigFileNotFoundException as e:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
384
|
-
)
|
|
358
|
+
critical(f"Config file not found at: {e}")
|
|
359
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
385
360
|
except ConfigInvalidException as e:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
389
|
-
)
|
|
361
|
+
critical(f"Invalid config:\n{e.underlying}")
|
|
362
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
390
363
|
except TestRequestFailedException as e:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
exit_code=EXIT_PROCESSING_ERROR,
|
|
394
|
-
)
|
|
364
|
+
critical(f"GitLab test request failed:\n{e.underlying}")
|
|
365
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
395
366
|
|
|
396
367
|
return gitlab, configuration
|
|
397
368
|
|
|
@@ -430,9 +401,8 @@ class GitLabForm:
|
|
|
430
401
|
"@",
|
|
431
402
|
group_number,
|
|
432
403
|
len(groups),
|
|
433
|
-
yellow,
|
|
404
|
+
"yellow",
|
|
434
405
|
f"Skipping group {group} as requested to start from {self.start_from_group}...",
|
|
435
|
-
reset,
|
|
436
406
|
)
|
|
437
407
|
continue
|
|
438
408
|
|
|
@@ -444,6 +414,7 @@ class GitLabForm:
|
|
|
444
414
|
"@",
|
|
445
415
|
group_number,
|
|
446
416
|
len(groups),
|
|
417
|
+
"black",
|
|
447
418
|
f"Processing group: {group}",
|
|
448
419
|
)
|
|
449
420
|
|
|
@@ -489,9 +460,8 @@ class GitLabForm:
|
|
|
489
460
|
"*",
|
|
490
461
|
project_number,
|
|
491
462
|
len(projects),
|
|
492
|
-
yellow,
|
|
463
|
+
"yellow",
|
|
493
464
|
f"Skipping project {project_and_group} as requested to start from {self.start_from}...",
|
|
494
|
-
reset,
|
|
495
465
|
)
|
|
496
466
|
continue
|
|
497
467
|
|
|
@@ -503,6 +473,7 @@ class GitLabForm:
|
|
|
503
473
|
"*",
|
|
504
474
|
project_number,
|
|
505
475
|
len(projects),
|
|
476
|
+
"black",
|
|
506
477
|
f"Processing project: {project_and_group}",
|
|
507
478
|
)
|
|
508
479
|
|
|
@@ -564,15 +535,15 @@ class GitLabForm:
|
|
|
564
535
|
|
|
565
536
|
local_version = package_version("gitlabform")
|
|
566
537
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
538
|
+
version_text = ""
|
|
539
|
+
# Legacy windows console cannot support unicode emoji rendering via Rich
|
|
540
|
+
if not console.legacy_windows:
|
|
541
|
+
version_text = "🏗 "
|
|
542
|
+
version_text += f"GitLabForm version: [bold blue]{local_version}[/]"
|
|
572
543
|
|
|
573
544
|
if skip_version_check:
|
|
574
|
-
# just print
|
|
575
|
-
print()
|
|
545
|
+
# just print version in use
|
|
546
|
+
console.print(version_text)
|
|
576
547
|
else:
|
|
577
548
|
try:
|
|
578
549
|
latest_version = luddite.get_version_pypi("gitlabform")
|
|
@@ -582,24 +553,26 @@ class GitLabForm:
|
|
|
582
553
|
error(f"Checking latest version failed:\n{e}")
|
|
583
554
|
return
|
|
584
555
|
|
|
556
|
+
latest_stable_text = "(the latest stable is {latest_version})"
|
|
557
|
+
|
|
585
558
|
if local_version == latest_version:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
559
|
+
version_info = "= the latest stable"
|
|
560
|
+
# Legacy windows console cannot support unicode emoji rendering via Rich
|
|
561
|
+
if not console.legacy_windows:
|
|
562
|
+
version_info = f"{version_info} ☺️"
|
|
590
563
|
elif version.parse(local_version) < version.parse(latest_version):
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
564
|
+
version_info = f"= outdated, please update"
|
|
565
|
+
if not console.legacy_windows:
|
|
566
|
+
version_info = f"{version_info} 😔"
|
|
567
|
+
version_info = f"{version_info}! {latest_stable_text}"
|
|
595
568
|
else:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
569
|
+
version_info = f"= pre-release: "
|
|
570
|
+
if not console.legacy_windows:
|
|
571
|
+
version_info = f"{version_info} 🤩"
|
|
572
|
+
version_info = f"{version_info} {latest_stable_text}"
|
|
600
573
|
|
|
601
574
|
# complete the line with a line ending
|
|
602
|
-
|
|
575
|
+
console.print(f"{version_text} {version_info}")
|
|
603
576
|
|
|
604
577
|
def _get_groups_and_projects(
|
|
605
578
|
self,
|
|
@@ -633,10 +606,8 @@ class GitLabForm:
|
|
|
633
606
|
error_message = "Configuration does not have any groups or projects defined!"
|
|
634
607
|
else:
|
|
635
608
|
error_message = f"Project or group {target} cannot be found in GitLab!"
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
639
|
-
)
|
|
609
|
+
critical(error_message)
|
|
610
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
640
611
|
|
|
641
612
|
self.groups_and_projects_filters.filter(groups, projects)
|
|
642
613
|
|
|
@@ -652,7 +623,7 @@ class GitLabForm:
|
|
|
652
623
|
|
|
653
624
|
:param entities: groups or projects
|
|
654
625
|
"""
|
|
655
|
-
|
|
626
|
+
info(f"# of {entities.name} to process: {len(entities.get_effective())}")
|
|
656
627
|
|
|
657
628
|
entities_omitted = ""
|
|
658
629
|
entities_verbose = f"{entities.name}: {entities.get_effective()}"
|
|
@@ -669,9 +640,9 @@ class GitLabForm:
|
|
|
669
640
|
entities_omitted += ")"
|
|
670
641
|
|
|
671
642
|
if entities_omitted:
|
|
672
|
-
|
|
643
|
+
info(entities_omitted)
|
|
673
644
|
|
|
674
|
-
|
|
645
|
+
info(entities_verbose)
|
|
675
646
|
|
|
676
647
|
@classmethod
|
|
677
648
|
def _show_summary(
|
|
@@ -695,50 +666,39 @@ class GitLabForm:
|
|
|
695
666
|
"""
|
|
696
667
|
|
|
697
668
|
if len(effective_groups) > 0 or len(effective_projects) > 0:
|
|
698
|
-
|
|
699
|
-
|
|
669
|
+
info(f"# of groups processed successfully: {successful_groups}")
|
|
670
|
+
info(f"# of projects processed successfully: {successful_projects}")
|
|
700
671
|
|
|
701
672
|
if len(failed_groups) > 0:
|
|
702
|
-
|
|
673
|
+
console.print(f"# of groups failed: {len(failed_groups)}", style="red")
|
|
703
674
|
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
|
|
675
|
+
console.print(f"Failed group {group_number}: {failed_groups[group_number]}", style="red")
|
|
707
676
|
if len(failed_projects) > 0:
|
|
708
|
-
#
|
|
709
|
-
info_1(red, f"# of projects failed: {len(failed_projects)}", reset)
|
|
710
|
-
# fmt: on
|
|
677
|
+
console.print(f"# of projects failed: {len(failed_projects)}", style="red")
|
|
711
678
|
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
|
|
679
|
+
console.print(f"Failed project {project_number}: {failed_projects[project_number]}", style="red")
|
|
715
680
|
|
|
716
681
|
if len(failed_groups) > 0 or len(failed_projects) > 0:
|
|
717
682
|
sys.exit(EXIT_PROCESSING_ERROR)
|
|
718
683
|
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
|
|
684
|
+
console.print("All requested groups/projects processed successfully! :sparkles:", style="green")
|
|
723
685
|
else:
|
|
724
|
-
|
|
725
|
-
info_1(yellow, "Nothing to do.", reset)
|
|
726
|
-
# fmt: on
|
|
686
|
+
console.print("Nothing to do.", style="yellow")
|
|
727
687
|
|
|
728
688
|
@classmethod
|
|
729
|
-
def _info_group_count(cls, prefix, i: int, n: int,
|
|
730
|
-
cls._info_count(purple, prefix, i, n,
|
|
689
|
+
def _info_group_count(cls, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
690
|
+
cls._info_count("purple", prefix, i, n, second_color, second_text)
|
|
731
691
|
|
|
732
692
|
@classmethod
|
|
733
|
-
def _info_project_count(cls, prefix, i: int, n: int,
|
|
734
|
-
cls._info_count(green, prefix, i, n,
|
|
693
|
+
def _info_project_count(cls, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
694
|
+
cls._info_count("green", prefix, i, n, second_color, second_text)
|
|
735
695
|
|
|
736
696
|
@classmethod
|
|
737
|
-
def _info_count(cls, color, prefix, i: int, n: int,
|
|
697
|
+
def _info_count(cls, color: str, prefix, i: int, n: int, second_color: str, second_text: str) -> None:
|
|
738
698
|
num_digits = len(str(n))
|
|
739
699
|
counter_format = f"(%{num_digits}d/%d)"
|
|
740
700
|
counter_str = counter_format % (i, n)
|
|
741
|
-
|
|
701
|
+
console.print(f"[bold {color}]{prefix} {counter_str}[/] [{second_color}]{second_text}[/]")
|
|
742
702
|
|
|
743
703
|
|
|
744
704
|
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
|
|
|
@@ -179,28 +178,38 @@ class ConfigurationCore(ABC):
|
|
|
179
178
|
|
|
180
179
|
merged_dict = merge({}, more_general_config, more_specific_config)
|
|
181
180
|
|
|
182
|
-
def break_inheritance(specific_config,
|
|
181
|
+
def break_inheritance(specific_config, parent_path=()):
|
|
182
|
+
"""
|
|
183
|
+
Walk the specific config tree and replace only the exact nested section
|
|
184
|
+
that declares ``inherit: false``.
|
|
185
|
+
|
|
186
|
+
``parent_path`` stores the full key path to the current section because
|
|
187
|
+
the same section name, like ``variables``, can appear in multiple branches.
|
|
188
|
+
Replacing by section name alone can therefore update the wrong subtree.
|
|
189
|
+
"""
|
|
183
190
|
for key, value in specific_config.items():
|
|
184
191
|
if "inherit" == key:
|
|
185
192
|
if not value:
|
|
186
|
-
|
|
193
|
+
replace_config_section(merged_dict, parent_path, specific_config)
|
|
187
194
|
break
|
|
188
195
|
elif value:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
192
|
-
)
|
|
193
|
-
elif type(value) in [CommentedMap, dict]:
|
|
194
|
-
break_inheritance(value, key)
|
|
195
|
-
|
|
196
|
-
def replace_config_sections(merged_config, specific_key, specific_config):
|
|
197
|
-
for key, value in merged_config.items():
|
|
198
|
-
if specific_key == key:
|
|
199
|
-
del specific_config["inherit"]
|
|
200
|
-
merged_config[key] = specific_config
|
|
201
|
-
break
|
|
196
|
+
critical(f"Cannot set the inheritance break flag with true\n")
|
|
197
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
202
198
|
elif type(value) in [CommentedMap, dict]:
|
|
203
|
-
|
|
199
|
+
break_inheritance(value, parent_path + (key,))
|
|
200
|
+
|
|
201
|
+
def replace_config_section(merged_config, parent_path, specific_config):
|
|
202
|
+
"""
|
|
203
|
+
Replace the merged section at ``parent_path`` with the specific section
|
|
204
|
+
that requested an inheritance break, dropping the control flag itself.
|
|
205
|
+
"""
|
|
206
|
+
target_config = merged_config
|
|
207
|
+
for key in parent_path[:-1]:
|
|
208
|
+
target_config = target_config[key]
|
|
209
|
+
|
|
210
|
+
replacement_config = deepcopy(specific_config)
|
|
211
|
+
replacement_config.pop("inherit", None)
|
|
212
|
+
target_config[parent_path[-1]] = replacement_config
|
|
204
213
|
|
|
205
214
|
break_inheritance(more_specific_config)
|
|
206
215
|
|
|
@@ -250,12 +259,12 @@ class ConfigurationCore(ABC):
|
|
|
250
259
|
if self.get(path, 0):
|
|
251
260
|
almost_duplicates = self._find_almost_duplicates_in(path)
|
|
252
261
|
if almost_duplicates:
|
|
253
|
-
|
|
262
|
+
critical(
|
|
254
263
|
f"There are almost duplicates in the keys of {path} - they differ only in case.\n"
|
|
255
264
|
f"They are: {', '.join(almost_duplicates)}\n"
|
|
256
|
-
f"This is not allowed as we ignore the case for group and project names."
|
|
257
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
265
|
+
f"This is not allowed as we ignore the case for group and project names."
|
|
258
266
|
)
|
|
267
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
259
268
|
|
|
260
269
|
def _find_almost_duplicates_in(self, configuration_path):
|
|
261
270
|
"""
|
|
@@ -310,7 +319,5 @@ class KeyNotFoundException(Exception):
|
|
|
310
319
|
if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
|
|
311
320
|
self.key = key
|
|
312
321
|
else:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
exit_code=EXIT_INVALID_INPUT,
|
|
316
|
-
)
|
|
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
|
|