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.
Files changed (79) hide show
  1. gitlabform/__init__.py +719 -0
  2. gitlabform/configuration/__init__.py +12 -0
  3. gitlabform/configuration/common.py +19 -0
  4. gitlabform/configuration/core.py +323 -0
  5. gitlabform/configuration/groups.py +127 -0
  6. gitlabform/configuration/projects.py +73 -0
  7. gitlabform/configuration/transform.py +259 -0
  8. gitlabform/constants.py +7 -0
  9. gitlabform/gitlab/__init__.py +108 -0
  10. gitlabform/gitlab/commits.py +39 -0
  11. gitlabform/gitlab/core.py +334 -0
  12. gitlabform/gitlab/group_badges.py +50 -0
  13. gitlabform/gitlab/group_ldap_links.py +40 -0
  14. gitlabform/gitlab/groups.py +96 -0
  15. gitlabform/gitlab/merge_requests.py +57 -0
  16. gitlabform/gitlab/pipelines.py +23 -0
  17. gitlabform/gitlab/project_badges.py +52 -0
  18. gitlabform/gitlab/project_deploy_keys.py +102 -0
  19. gitlabform/gitlab/project_merge_requests_approvals.py +94 -0
  20. gitlabform/gitlab/project_protected_environments.py +37 -0
  21. gitlabform/gitlab/projects.py +151 -0
  22. gitlabform/gitlab/python_gitlab.py +251 -0
  23. gitlabform/gitlab/variables.py +47 -0
  24. gitlabform/lists/__init__.py +62 -0
  25. gitlabform/lists/filter.py +99 -0
  26. gitlabform/lists/groups.py +87 -0
  27. gitlabform/lists/projects.py +239 -0
  28. gitlabform/output.py +46 -0
  29. gitlabform/processors/__init__.py +43 -0
  30. gitlabform/processors/abstract_processor.py +187 -0
  31. gitlabform/processors/application/__init__.py +17 -0
  32. gitlabform/processors/application/application_settings_processor.py +39 -0
  33. gitlabform/processors/defining_keys.py +152 -0
  34. gitlabform/processors/group/__init__.py +48 -0
  35. gitlabform/processors/group/group_badges_processor.py +17 -0
  36. gitlabform/processors/group/group_hooks_processor.py +75 -0
  37. gitlabform/processors/group/group_labels_processor.py +28 -0
  38. gitlabform/processors/group/group_ldap_links_processor.py +16 -0
  39. gitlabform/processors/group/group_members_processor.py +287 -0
  40. gitlabform/processors/group/group_push_rules_processor.py +44 -0
  41. gitlabform/processors/group/group_saml_links_processor.py +65 -0
  42. gitlabform/processors/group/group_settings_processor.py +90 -0
  43. gitlabform/processors/group/group_variables_processor.py +26 -0
  44. gitlabform/processors/multiple_entities_processor.py +171 -0
  45. gitlabform/processors/project/__init__.py +80 -0
  46. gitlabform/processors/project/badges_processor.py +17 -0
  47. gitlabform/processors/project/branches_processor.py +514 -0
  48. gitlabform/processors/project/deploy_keys_processor.py +18 -0
  49. gitlabform/processors/project/files_processor.py +301 -0
  50. gitlabform/processors/project/hooks_processor.py +64 -0
  51. gitlabform/processors/project/integrations_processor.py +33 -0
  52. gitlabform/processors/project/job_token_scope_processor.py +216 -0
  53. gitlabform/processors/project/members_processor.py +204 -0
  54. gitlabform/processors/project/merge_requests_approval_rules.py +17 -0
  55. gitlabform/processors/project/merge_requests_approvals.py +59 -0
  56. gitlabform/processors/project/project_labels_processor.py +27 -0
  57. gitlabform/processors/project/project_processor.py +62 -0
  58. gitlabform/processors/project/project_push_rules_processor.py +52 -0
  59. gitlabform/processors/project/project_security_settings.py +66 -0
  60. gitlabform/processors/project/project_settings_processor.py +239 -0
  61. gitlabform/processors/project/project_variables_processor.py +94 -0
  62. gitlabform/processors/project/remote_mirrors_processor.py +278 -0
  63. gitlabform/processors/project/resource_groups_processor.py +48 -0
  64. gitlabform/processors/project/schedules_processor.py +208 -0
  65. gitlabform/processors/project/tags_processor.py +108 -0
  66. gitlabform/processors/shared/__init__.py +0 -0
  67. gitlabform/processors/shared/protected_environments_processor.py +20 -0
  68. gitlabform/processors/util/__init__.py +0 -0
  69. gitlabform/processors/util/decorators.py +44 -0
  70. gitlabform/processors/util/difference_logger.py +70 -0
  71. gitlabform/processors/util/labels_processor.py +120 -0
  72. gitlabform/processors/util/variables_processor.py +143 -0
  73. gitlabform/run.py +9 -0
  74. gitlabform/util.py +7 -0
  75. gitlabform-0.0.540a0.dist-info/METADATA +54 -0
  76. gitlabform-0.0.540a0.dist-info/RECORD +79 -0
  77. gitlabform-0.0.540a0.dist-info/WHEEL +4 -0
  78. gitlabform-0.0.540a0.dist-info/entry_points.txt +9 -0
  79. 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}'.")