gitlabform 0.0.540a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitlabform/__init__.py +719 -0
- gitlabform/configuration/__init__.py +12 -0
- gitlabform/configuration/common.py +19 -0
- gitlabform/configuration/core.py +323 -0
- gitlabform/configuration/groups.py +127 -0
- gitlabform/configuration/projects.py +73 -0
- gitlabform/configuration/transform.py +259 -0
- gitlabform/constants.py +7 -0
- gitlabform/gitlab/__init__.py +108 -0
- gitlabform/gitlab/commits.py +39 -0
- gitlabform/gitlab/core.py +334 -0
- gitlabform/gitlab/group_badges.py +50 -0
- gitlabform/gitlab/group_ldap_links.py +40 -0
- gitlabform/gitlab/groups.py +96 -0
- gitlabform/gitlab/merge_requests.py +57 -0
- gitlabform/gitlab/pipelines.py +23 -0
- gitlabform/gitlab/project_badges.py +52 -0
- gitlabform/gitlab/project_deploy_keys.py +102 -0
- gitlabform/gitlab/project_merge_requests_approvals.py +94 -0
- gitlabform/gitlab/project_protected_environments.py +37 -0
- gitlabform/gitlab/projects.py +151 -0
- gitlabform/gitlab/python_gitlab.py +251 -0
- gitlabform/gitlab/variables.py +47 -0
- gitlabform/lists/__init__.py +62 -0
- gitlabform/lists/filter.py +99 -0
- gitlabform/lists/groups.py +87 -0
- gitlabform/lists/projects.py +239 -0
- gitlabform/output.py +46 -0
- gitlabform/processors/__init__.py +43 -0
- gitlabform/processors/abstract_processor.py +187 -0
- gitlabform/processors/application/__init__.py +17 -0
- gitlabform/processors/application/application_settings_processor.py +39 -0
- gitlabform/processors/defining_keys.py +152 -0
- gitlabform/processors/group/__init__.py +48 -0
- gitlabform/processors/group/group_badges_processor.py +17 -0
- gitlabform/processors/group/group_hooks_processor.py +75 -0
- gitlabform/processors/group/group_labels_processor.py +28 -0
- gitlabform/processors/group/group_ldap_links_processor.py +16 -0
- gitlabform/processors/group/group_members_processor.py +287 -0
- gitlabform/processors/group/group_push_rules_processor.py +44 -0
- gitlabform/processors/group/group_saml_links_processor.py +65 -0
- gitlabform/processors/group/group_settings_processor.py +90 -0
- gitlabform/processors/group/group_variables_processor.py +26 -0
- gitlabform/processors/multiple_entities_processor.py +171 -0
- gitlabform/processors/project/__init__.py +80 -0
- gitlabform/processors/project/badges_processor.py +17 -0
- gitlabform/processors/project/branches_processor.py +514 -0
- gitlabform/processors/project/deploy_keys_processor.py +18 -0
- gitlabform/processors/project/files_processor.py +301 -0
- gitlabform/processors/project/hooks_processor.py +64 -0
- gitlabform/processors/project/integrations_processor.py +33 -0
- gitlabform/processors/project/job_token_scope_processor.py +216 -0
- gitlabform/processors/project/members_processor.py +204 -0
- gitlabform/processors/project/merge_requests_approval_rules.py +17 -0
- gitlabform/processors/project/merge_requests_approvals.py +59 -0
- gitlabform/processors/project/project_labels_processor.py +27 -0
- gitlabform/processors/project/project_processor.py +62 -0
- gitlabform/processors/project/project_push_rules_processor.py +52 -0
- gitlabform/processors/project/project_security_settings.py +66 -0
- gitlabform/processors/project/project_settings_processor.py +239 -0
- gitlabform/processors/project/project_variables_processor.py +94 -0
- gitlabform/processors/project/remote_mirrors_processor.py +278 -0
- gitlabform/processors/project/resource_groups_processor.py +48 -0
- gitlabform/processors/project/schedules_processor.py +208 -0
- gitlabform/processors/project/tags_processor.py +108 -0
- gitlabform/processors/shared/__init__.py +0 -0
- gitlabform/processors/shared/protected_environments_processor.py +20 -0
- gitlabform/processors/util/__init__.py +0 -0
- gitlabform/processors/util/decorators.py +44 -0
- gitlabform/processors/util/difference_logger.py +70 -0
- gitlabform/processors/util/labels_processor.py +120 -0
- gitlabform/processors/util/variables_processor.py +143 -0
- gitlabform/run.py +9 -0
- gitlabform/util.py +7 -0
- gitlabform-0.0.540a0.dist-info/METADATA +54 -0
- gitlabform-0.0.540a0.dist-info/RECORD +79 -0
- gitlabform-0.0.540a0.dist-info/WHEEL +4 -0
- gitlabform-0.0.540a0.dist-info/entry_points.txt +9 -0
- gitlabform-0.0.540a0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from typing import DefaultDict
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@enum.unique
|
|
14
|
+
class OmissionReason(Enum):
|
|
15
|
+
ARCHIVED = "archived"
|
|
16
|
+
SCHEDULED_FOR_DELETION = "scheduled for deletion"
|
|
17
|
+
EMPTY = "empty effective config"
|
|
18
|
+
SKIPPED = "skipped"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Entities(ABC):
|
|
22
|
+
"""
|
|
23
|
+
Entities here are lists of GitLab groups or projects on which GitLabForm will operate.
|
|
24
|
+
The code here assumes that there is a set of requested entities, but some of them may be omitted
|
|
25
|
+
for various reasons, so we want to be able to get only the effective set of them.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, name: str):
|
|
29
|
+
self.requested: set = set()
|
|
30
|
+
self.omitted: DefaultDict[OmissionReason, set] = defaultdict(set)
|
|
31
|
+
self.name = name
|
|
32
|
+
|
|
33
|
+
def add_requested(self, more_requested: list) -> None:
|
|
34
|
+
self.requested = self.requested | set(more_requested)
|
|
35
|
+
|
|
36
|
+
def add_omitted(self, reason: OmissionReason, more_omitted: list) -> None:
|
|
37
|
+
self.omitted[reason] = self.omitted[reason] | set(more_omitted)
|
|
38
|
+
|
|
39
|
+
def get_omitted(self, reason: OmissionReason) -> list:
|
|
40
|
+
return sorted(self.omitted[reason])
|
|
41
|
+
|
|
42
|
+
def any_omitted(self) -> bool:
|
|
43
|
+
for reason in self.omitted:
|
|
44
|
+
if len(self.omitted[reason]) > 0:
|
|
45
|
+
return True
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
def get_effective(self) -> list:
|
|
49
|
+
effective_set = copy.deepcopy(self.requested)
|
|
50
|
+
for reason in self.omitted:
|
|
51
|
+
effective_set -= self.omitted[reason]
|
|
52
|
+
return sorted(effective_set)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Groups(Entities):
|
|
56
|
+
def __init__(self):
|
|
57
|
+
super().__init__("groups")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Projects(Entities):
|
|
61
|
+
def __init__(self):
|
|
62
|
+
super().__init__("projects")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from logging import critical
|
|
5
|
+
|
|
6
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
7
|
+
from gitlabform.lists import OmissionReason, Groups, Projects
|
|
8
|
+
|
|
9
|
+
# Groups and projects filters jobs is to omit some groups and projects that GitLabForm is requested
|
|
10
|
+
# to process for a speed-up.
|
|
11
|
+
#
|
|
12
|
+
# An example reason for omitting groups and projects is when they have an empty effective config.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GroupsAndProjectsFilters:
|
|
16
|
+
def __init__(self, configuration, group_processors, project_processors):
|
|
17
|
+
self.configuration = configuration
|
|
18
|
+
|
|
19
|
+
self.omit_empty_configs = OmitEmptyConfigs(configuration, group_processors, project_processors)
|
|
20
|
+
# add next filters here
|
|
21
|
+
|
|
22
|
+
def filter(self, groups: Groups, projects: Projects):
|
|
23
|
+
self.omit_empty_configs.filter(groups, projects)
|
|
24
|
+
# add next filters here
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GroupsAndProjectsFilter(ABC):
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def filter(self, groups: Groups, projects: Projects):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OmitEmptyConfigs(GroupsAndProjectsFilter):
|
|
34
|
+
"""
|
|
35
|
+
One of the reasons groups or project can be omitted from processing is when they have an empty effective config.
|
|
36
|
+
|
|
37
|
+
For example with a config like:
|
|
38
|
+
|
|
39
|
+
projects_and_groups:
|
|
40
|
+
*:
|
|
41
|
+
group_variables:
|
|
42
|
+
foobar:
|
|
43
|
+
key: "a key"
|
|
44
|
+
value: "the value"
|
|
45
|
+
|
|
46
|
+
...and a request query "foo/bar" that points to a project "bar" in a group "foo", the effective config
|
|
47
|
+
is empty, as "group_variables" is a group-level configuration and "foo/bar" is a project.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, configuration, group_processors, project_processors):
|
|
51
|
+
self.configuration = configuration
|
|
52
|
+
self.group_processors = group_processors
|
|
53
|
+
self.project_processors = project_processors
|
|
54
|
+
|
|
55
|
+
if not self.configuration.get("projects_and_groups", {}):
|
|
56
|
+
critical("Configuration has to contain non-empty 'projects_and_groups' key.")
|
|
57
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
58
|
+
|
|
59
|
+
def filter(self, groups: Groups, projects: Projects) -> None:
|
|
60
|
+
"""
|
|
61
|
+
:param groups: list of groups (and possibly subgroups)
|
|
62
|
+
:param projects: list of projects
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
groups_with_empty_configs = []
|
|
66
|
+
for group in groups.get_effective():
|
|
67
|
+
if self._group_has_empty_effective_config(group):
|
|
68
|
+
groups_with_empty_configs.append(group)
|
|
69
|
+
groups.add_omitted(OmissionReason.EMPTY, groups_with_empty_configs)
|
|
70
|
+
|
|
71
|
+
projects_with_empty_configs = []
|
|
72
|
+
for project in projects.get_effective():
|
|
73
|
+
if self._project_has_empty_effective_config(project):
|
|
74
|
+
projects_with_empty_configs.append(project)
|
|
75
|
+
projects.add_omitted(OmissionReason.EMPTY, projects_with_empty_configs)
|
|
76
|
+
|
|
77
|
+
def _group_has_empty_effective_config(self, group: str) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
:param group: group/subgroup
|
|
80
|
+
:return: if given group/subgroup has no config that can be processed
|
|
81
|
+
by any group-level processors
|
|
82
|
+
"""
|
|
83
|
+
config_for_group = self.configuration.get_effective_config_for_group(group)
|
|
84
|
+
for configuration_name in config_for_group.keys():
|
|
85
|
+
if configuration_name in self.group_processors.get_configuration_names():
|
|
86
|
+
return False
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
def _project_has_empty_effective_config(self, project: str) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
:param project: 'group/project'
|
|
92
|
+
:return: if given project has no config that can be processed
|
|
93
|
+
by any project-level processors
|
|
94
|
+
"""
|
|
95
|
+
config_for_project = self.configuration.get_effective_config_for_project(project)
|
|
96
|
+
for configuration_name in config_for_project.keys():
|
|
97
|
+
if configuration_name in self.project_processors.get_configuration_names():
|
|
98
|
+
return False
|
|
99
|
+
return True
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import critical
|
|
3
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
4
|
+
from gitlabform.gitlab.core import NotFoundException
|
|
5
|
+
from gitlabform.lists import OmissionReason, Groups
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GroupsProvider:
|
|
9
|
+
"""
|
|
10
|
+
For a query like "project/group", "group", "group/subgroup", ALL or ALL_DEFINED this
|
|
11
|
+
class gets the effective lists of groups, taking into account skipped groups
|
|
12
|
+
and the fact that the group and project names case are somewhat case-sensitive.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, gitlab, configuration, recurse_subgroups):
|
|
16
|
+
self.gitlab = gitlab
|
|
17
|
+
self.configuration = configuration
|
|
18
|
+
self.recurse_subgroups = recurse_subgroups
|
|
19
|
+
|
|
20
|
+
def get_groups(self, target: str) -> Groups:
|
|
21
|
+
"""
|
|
22
|
+
:param target: "project/group", "group", "group/subgroup", ALL or ALL_DEFINED
|
|
23
|
+
:return: Groups
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if target not in ["ALL", "ALL_DEFINED"]:
|
|
27
|
+
groups = self._get_single_group(target, self.recurse_subgroups)
|
|
28
|
+
else:
|
|
29
|
+
groups = self._get_groups(target)
|
|
30
|
+
|
|
31
|
+
return groups
|
|
32
|
+
|
|
33
|
+
def _get_single_group(self, target: str, recurse_subgroups: bool) -> Groups:
|
|
34
|
+
groups = Groups()
|
|
35
|
+
|
|
36
|
+
# it may be a subgroup or a group...
|
|
37
|
+
try:
|
|
38
|
+
maybe_group = self.gitlab.get_group_case_insensitive(target)
|
|
39
|
+
path = maybe_group["full_path"]
|
|
40
|
+
groups.add_requested([path])
|
|
41
|
+
|
|
42
|
+
if recurse_subgroups:
|
|
43
|
+
descendants = self.gitlab.get_group_descendants(path)
|
|
44
|
+
groups.add_requested([group["full_path"] for group in descendants])
|
|
45
|
+
|
|
46
|
+
groups.add_omitted(
|
|
47
|
+
OmissionReason.SKIPPED,
|
|
48
|
+
self._get_skipped_groups(groups.get_effective()),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
except NotFoundException:
|
|
52
|
+
# ...or a single project, which we ignore here
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
return groups
|
|
56
|
+
|
|
57
|
+
def _get_groups(self, target: str) -> Groups:
|
|
58
|
+
groups = Groups()
|
|
59
|
+
|
|
60
|
+
if target == "ALL":
|
|
61
|
+
groups.add_requested(self.gitlab.get_groups())
|
|
62
|
+
else: # ALL_DEFINED
|
|
63
|
+
groups.add_requested(self.configuration.get_groups())
|
|
64
|
+
|
|
65
|
+
groups.add_omitted(OmissionReason.SKIPPED, self._get_skipped_groups(groups.get_effective()))
|
|
66
|
+
|
|
67
|
+
if target == "ALL_DEFINED":
|
|
68
|
+
# check if all the groups from the config actually exist
|
|
69
|
+
self._verify_if_groups_exist(groups.get_effective())
|
|
70
|
+
|
|
71
|
+
return groups
|
|
72
|
+
|
|
73
|
+
def _verify_if_groups_exist(self, groups: list):
|
|
74
|
+
for group in groups:
|
|
75
|
+
try:
|
|
76
|
+
self.gitlab.get_group_case_insensitive(group)
|
|
77
|
+
except NotFoundException:
|
|
78
|
+
critical(f"Configuration contains group {group} but it cannot be found in GitLab!")
|
|
79
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
80
|
+
|
|
81
|
+
def _get_skipped_groups(self, groups: list) -> list:
|
|
82
|
+
skipped = []
|
|
83
|
+
for group in groups:
|
|
84
|
+
if self.configuration.is_group_skipped(group):
|
|
85
|
+
skipped.append(group)
|
|
86
|
+
|
|
87
|
+
return skipped
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
from logging import debug
|
|
4
|
+
from logging import critical
|
|
5
|
+
|
|
6
|
+
from gitlabform.constants import EXIT_INVALID_INPUT
|
|
7
|
+
from gitlabform.lists import OmissionReason, Groups, Projects
|
|
8
|
+
from gitlabform.lists.groups import GroupsProvider
|
|
9
|
+
|
|
10
|
+
from gitlabform.gitlab.core import NotFoundException
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProjectsProvider(GroupsProvider):
|
|
14
|
+
"""
|
|
15
|
+
For a query like "project/group", "group", "group/subgroup", ALL or ALL_DEFINED this
|
|
16
|
+
class gets the effective lists of projects, taking into account skipped projects
|
|
17
|
+
and the fact that the group and project names case are somewhat case-sensitive.
|
|
18
|
+
|
|
19
|
+
Because the projects depend on groups requested, this class inherits GroupsProvider.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
gitlab,
|
|
25
|
+
configuration,
|
|
26
|
+
include_archived_projects,
|
|
27
|
+
include_projects_scheduled_for_deletion,
|
|
28
|
+
recurse_subgroups,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(gitlab, configuration, recurse_subgroups)
|
|
31
|
+
self.include_archived_projects = include_archived_projects
|
|
32
|
+
self.include_projects_scheduled_for_deletion = include_projects_scheduled_for_deletion
|
|
33
|
+
|
|
34
|
+
def get_projects(self, target: str) -> Projects:
|
|
35
|
+
"""
|
|
36
|
+
:param target: "project/group", "group", "group/subgroup", ALL or ALL_DEFINED
|
|
37
|
+
:return: Projects
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
groups = self.get_groups(target)
|
|
41
|
+
|
|
42
|
+
if target not in ["ALL", "ALL_DEFINED"]:
|
|
43
|
+
if len(groups.get_effective()) == 1:
|
|
44
|
+
projects = self._get_projects(target, groups)
|
|
45
|
+
else:
|
|
46
|
+
projects = self._get_single_project(target)
|
|
47
|
+
else:
|
|
48
|
+
projects = self._get_projects(target, groups)
|
|
49
|
+
|
|
50
|
+
return projects
|
|
51
|
+
|
|
52
|
+
def _get_single_project(self, target: str) -> Projects:
|
|
53
|
+
projects = Projects()
|
|
54
|
+
|
|
55
|
+
# it may be a single project
|
|
56
|
+
try:
|
|
57
|
+
maybe_project = self.gitlab.get_project_case_insensitive(target)
|
|
58
|
+
projects.add_requested([maybe_project["path_with_namespace"]])
|
|
59
|
+
|
|
60
|
+
except NotFoundException:
|
|
61
|
+
debug("Could not find '%s'", target)
|
|
62
|
+
if project_transfer_source := self._get_project_transfer_source_from_config(target):
|
|
63
|
+
if self._find_project_transfer_source_in_gitlab(project_transfer_source):
|
|
64
|
+
projects.add_requested([target])
|
|
65
|
+
else:
|
|
66
|
+
critical(
|
|
67
|
+
f"""Configuration contains project {target} to be transferred from {project_transfer_source}
|
|
68
|
+
but the source project cannot be found in GitLab!"""
|
|
69
|
+
)
|
|
70
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
71
|
+
|
|
72
|
+
return projects
|
|
73
|
+
|
|
74
|
+
def _get_projects(self, target: str, groups: Groups) -> Projects:
|
|
75
|
+
projects = Projects()
|
|
76
|
+
|
|
77
|
+
# the source of projects are the *effective* requested groups
|
|
78
|
+
(
|
|
79
|
+
projects_from_groups,
|
|
80
|
+
archived_projects_from_groups,
|
|
81
|
+
scheduled_for_deletion_projects_from_groups,
|
|
82
|
+
) = self._get_all_and_omitted_projects_from_groups(groups.get_effective())
|
|
83
|
+
projects.add_requested(projects_from_groups)
|
|
84
|
+
projects.add_omitted(OmissionReason.ARCHIVED, archived_projects_from_groups)
|
|
85
|
+
projects.add_omitted(OmissionReason.SCHEDULED_FOR_DELETION, scheduled_for_deletion_projects_from_groups)
|
|
86
|
+
|
|
87
|
+
projects_from_configuration = self.configuration.get_projects()
|
|
88
|
+
|
|
89
|
+
# TODO: this check should be case-insensitive
|
|
90
|
+
projects_from_configuration_not_from_groups = [
|
|
91
|
+
project for project in projects_from_configuration if project not in projects.requested
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
if target == "ALL_DEFINED":
|
|
95
|
+
# in this case we also need to get the list of projects explicitly
|
|
96
|
+
# defined in the configuration, but we don't need to re-check for
|
|
97
|
+
# being archived or scheduled for deletion projects that we already got from groups
|
|
98
|
+
|
|
99
|
+
(
|
|
100
|
+
archived_projects_from_configuration_not_from_groups,
|
|
101
|
+
scheduled_for_deletion_projects_from_configuration_not_from_groups,
|
|
102
|
+
) = self._verify_if_projects_exist_and_get_omitted_projects(projects_from_configuration_not_from_groups)
|
|
103
|
+
|
|
104
|
+
projects.add_requested(projects_from_configuration_not_from_groups)
|
|
105
|
+
projects.add_omitted(
|
|
106
|
+
OmissionReason.ARCHIVED,
|
|
107
|
+
archived_projects_from_configuration_not_from_groups,
|
|
108
|
+
)
|
|
109
|
+
projects.add_omitted(
|
|
110
|
+
OmissionReason.SCHEDULED_FOR_DELETION,
|
|
111
|
+
scheduled_for_deletion_projects_from_configuration_not_from_groups,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
# in all other cases, we also need to look for projects in the config
|
|
115
|
+
# that are being transferred to a different namespace
|
|
116
|
+
for project in projects_from_configuration_not_from_groups:
|
|
117
|
+
# if the target is a group, and the project is not in the group
|
|
118
|
+
if len(groups.get_effective()) == 1 and target != "ALL" and target != project.rsplit("/", 1)[0]:
|
|
119
|
+
debug(
|
|
120
|
+
"Ignore project '%s', since it's not in target group '%s",
|
|
121
|
+
project,
|
|
122
|
+
target,
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
if (maybe_projects := self._get_single_project(project)) and len(
|
|
126
|
+
maybe_projects.get_effective()
|
|
127
|
+
) != 0:
|
|
128
|
+
projects.add_requested([project])
|
|
129
|
+
|
|
130
|
+
# TODO: consider checking for skipped earlier to avoid making requests for projects that will be skipped anyway
|
|
131
|
+
projects.add_omitted(OmissionReason.SKIPPED, self._get_skipped_projects(projects.get_effective()))
|
|
132
|
+
|
|
133
|
+
return projects
|
|
134
|
+
|
|
135
|
+
def _verify_if_projects_exist_and_get_omitted_projects(self, projects: list) -> Tuple[list, list]:
|
|
136
|
+
archived = []
|
|
137
|
+
scheduled_for_deletion = []
|
|
138
|
+
for project in projects:
|
|
139
|
+
try:
|
|
140
|
+
project_object = self.gitlab.get_project_case_insensitive(project)
|
|
141
|
+
if not self.include_archived_projects and project_object["archived"]:
|
|
142
|
+
archived.append(project_object["path_with_namespace"])
|
|
143
|
+
if not self.include_projects_scheduled_for_deletion and project_object.get("marked_for_deletion_on"):
|
|
144
|
+
scheduled_for_deletion.append(project_object["path_with_namespace"])
|
|
145
|
+
except NotFoundException:
|
|
146
|
+
debug("Could not find '%s'", project)
|
|
147
|
+
if project_transfer_source := self._get_project_transfer_source_from_config(project):
|
|
148
|
+
source_project = self._find_project_transfer_source_in_gitlab(project_transfer_source)
|
|
149
|
+
if source_project:
|
|
150
|
+
if not self.include_archived_projects and source_project["archived"]:
|
|
151
|
+
archived.append(project)
|
|
152
|
+
if not self.include_projects_scheduled_for_deletion and source_project.get(
|
|
153
|
+
"marked_for_deletion_on"
|
|
154
|
+
):
|
|
155
|
+
scheduled_for_deletion.append(project)
|
|
156
|
+
else:
|
|
157
|
+
critical(
|
|
158
|
+
f"""Configuration contains project {project} to be transferred from {project_transfer_source}
|
|
159
|
+
but the source project cannot be found in GitLab!"""
|
|
160
|
+
)
|
|
161
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
162
|
+
else:
|
|
163
|
+
critical(
|
|
164
|
+
f"Configuration contains project {project} but it cannot be found in GitLab and it's not a project that is configured to be transferred from elsewhere."
|
|
165
|
+
)
|
|
166
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
167
|
+
|
|
168
|
+
return archived, scheduled_for_deletion
|
|
169
|
+
|
|
170
|
+
def _get_all_and_omitted_projects_from_groups(self, groups: list) -> Tuple[list, list, list]:
|
|
171
|
+
all = []
|
|
172
|
+
archived = []
|
|
173
|
+
scheduled_for_deletion = []
|
|
174
|
+
for group in groups:
|
|
175
|
+
if self.include_archived_projects and self.include_projects_scheduled_for_deletion:
|
|
176
|
+
all += self.gitlab.get_projects(group, include_archived=True)
|
|
177
|
+
else:
|
|
178
|
+
project_objects = self.gitlab.get_projects(group, include_archived=True, only_names=False)
|
|
179
|
+
for project_object in project_objects:
|
|
180
|
+
project = project_object["path_with_namespace"]
|
|
181
|
+
all.append(project)
|
|
182
|
+
if not self.include_archived_projects and project_object["archived"]:
|
|
183
|
+
archived.append(project)
|
|
184
|
+
if not self.include_projects_scheduled_for_deletion and project_object.get(
|
|
185
|
+
"marked_for_deletion_on"
|
|
186
|
+
):
|
|
187
|
+
scheduled_for_deletion.append(project)
|
|
188
|
+
|
|
189
|
+
# deduplicate as we may have a group X and its subgroup X/Y in the groups list so the effective projects
|
|
190
|
+
# may occur more than once
|
|
191
|
+
return list(set(all)), list(set(archived)), list(set(scheduled_for_deletion))
|
|
192
|
+
|
|
193
|
+
def _get_skipped_projects(self, projects: list) -> list:
|
|
194
|
+
skipped = []
|
|
195
|
+
for project in projects:
|
|
196
|
+
if self.configuration.is_project_skipped(project):
|
|
197
|
+
skipped.append(project)
|
|
198
|
+
|
|
199
|
+
return skipped
|
|
200
|
+
|
|
201
|
+
def _get_project_transfer_source_from_config(self, project: str) -> Optional[str]:
|
|
202
|
+
try:
|
|
203
|
+
debug(
|
|
204
|
+
"Checking if project '%s' needs to be transferred from elsewhere",
|
|
205
|
+
project,
|
|
206
|
+
)
|
|
207
|
+
project_transfer_source = self.configuration.config["projects_and_groups"][project]["project"][
|
|
208
|
+
"transfer_from"
|
|
209
|
+
]
|
|
210
|
+
debug(
|
|
211
|
+
"Project '%s' needs to be transferred from '%s'",
|
|
212
|
+
project,
|
|
213
|
+
project_transfer_source,
|
|
214
|
+
)
|
|
215
|
+
except KeyError:
|
|
216
|
+
debug("'%s' is not a project needing to be transferred", project)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
return project_transfer_source
|
|
220
|
+
|
|
221
|
+
def _find_project_transfer_source_in_gitlab(self, project_transfer_source: str) -> Optional[dict]:
|
|
222
|
+
try:
|
|
223
|
+
debug(
|
|
224
|
+
"Checking if project transfer source ('%s') exists in GitLab",
|
|
225
|
+
project_transfer_source,
|
|
226
|
+
)
|
|
227
|
+
maybe_project = self.gitlab.get_project_case_insensitive(project_transfer_source)
|
|
228
|
+
debug(
|
|
229
|
+
"Project transfer source ('%s') exists",
|
|
230
|
+
project_transfer_source,
|
|
231
|
+
)
|
|
232
|
+
except NotFoundException:
|
|
233
|
+
debug(
|
|
234
|
+
"Project transfer source ('%s') does not exist",
|
|
235
|
+
project_transfer_source,
|
|
236
|
+
)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
return maybe_project
|
gitlabform/output.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from logging import debug, critical, info
|
|
3
|
+
|
|
4
|
+
import ez_yaml
|
|
5
|
+
|
|
6
|
+
from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EffectiveConfigurationFile:
|
|
10
|
+
"""
|
|
11
|
+
For GitLabForm upgrades and configuration refactoring we want to be able to compare the effective configurations
|
|
12
|
+
before and after the code/app change. This class provides a feature to write the effective configuration into a YAML
|
|
13
|
+
file.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, output_file):
|
|
17
|
+
if output_file:
|
|
18
|
+
try:
|
|
19
|
+
self.output_file = open(output_file, "w")
|
|
20
|
+
debug(f"Opened file {self.output_file} to write the effective configs to.")
|
|
21
|
+
except Exception as e:
|
|
22
|
+
critical(f"Error when trying to open {self.output_file} to write the effective configs to: {e}")
|
|
23
|
+
sys.exit(EXIT_INVALID_INPUT)
|
|
24
|
+
else:
|
|
25
|
+
self.output_file = None
|
|
26
|
+
|
|
27
|
+
self.config = {}
|
|
28
|
+
|
|
29
|
+
def add_placeholder(self, project_or_group: str):
|
|
30
|
+
if self.output_file:
|
|
31
|
+
self.config[project_or_group] = {}
|
|
32
|
+
|
|
33
|
+
def add_configuration(self, project_or_group: str, configuration_name: str, configuration: dict):
|
|
34
|
+
if self.output_file:
|
|
35
|
+
info(f"Adding effective configuration for {configuration_name}.")
|
|
36
|
+
self.config[project_or_group][configuration_name] = configuration
|
|
37
|
+
|
|
38
|
+
def write_to_file(self):
|
|
39
|
+
if self.output_file:
|
|
40
|
+
try:
|
|
41
|
+
yaml_configuration = ez_yaml.to_string(self.config)
|
|
42
|
+
self.output_file.write(yaml_configuration)
|
|
43
|
+
self.output_file.close()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
critical(f"Error when trying to write or close {self.output_file}: {e}")
|
|
46
|
+
sys.exit(EXIT_PROCESSING_ERROR)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
|
|
3
|
+
from logging import info
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from gitlabform.configuration import Configuration
|
|
8
|
+
from gitlabform.gitlab import GitLab
|
|
9
|
+
from gitlabform.output import EffectiveConfigurationFile
|
|
10
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbstractProcessors(ABC):
|
|
14
|
+
def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
|
|
15
|
+
self.processors: List[AbstractProcessor] = []
|
|
16
|
+
|
|
17
|
+
def get_configuration_names(self):
|
|
18
|
+
return [processor.configuration_name for processor in self.processors]
|
|
19
|
+
|
|
20
|
+
def process_entity(
|
|
21
|
+
self,
|
|
22
|
+
entity_reference: str,
|
|
23
|
+
configuration: dict,
|
|
24
|
+
dry_run: bool,
|
|
25
|
+
diff_only_changed: bool,
|
|
26
|
+
effective_configuration: EffectiveConfigurationFile,
|
|
27
|
+
only_sections: List[str] | str,
|
|
28
|
+
exclude_sections: List[str],
|
|
29
|
+
):
|
|
30
|
+
for processor in self.processors:
|
|
31
|
+
if processor.configuration_name not in exclude_sections:
|
|
32
|
+
if only_sections == "all" or processor.configuration_name in only_sections:
|
|
33
|
+
processor.process(
|
|
34
|
+
entity_reference,
|
|
35
|
+
configuration,
|
|
36
|
+
dry_run,
|
|
37
|
+
diff_only_changed,
|
|
38
|
+
effective_configuration,
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
info(f"Skipping section '{processor.configuration_name}' - not in --only-sections list.")
|
|
42
|
+
else:
|
|
43
|
+
info(f"Excluding section '{processor.configuration_name}'.")
|