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,187 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from logging import debug
|
|
3
|
+
from typing import Callable, Union
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from logging import info
|
|
7
|
+
|
|
8
|
+
from gitlabform.gitlab import GitLab, PythonGitlab
|
|
9
|
+
from gitlabform.gitlab import GitlabWrapper
|
|
10
|
+
from gitlabform.output import EffectiveConfigurationFile
|
|
11
|
+
from gitlabform.processors.util.decorators import configuration_to_safe_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AbstractProcessor(ABC):
|
|
15
|
+
def __init__(self, configuration_name: str, gitlab: GitLab):
|
|
16
|
+
self.configuration_name = configuration_name
|
|
17
|
+
self.gitlab = gitlab
|
|
18
|
+
self.custom_diff_analyzers: dict[
|
|
19
|
+
str,
|
|
20
|
+
Callable[[str, list[dict[str, Union[str, int]]], list[dict[str, int]]], bool],
|
|
21
|
+
] = {}
|
|
22
|
+
self.gl: PythonGitlab = GitlabWrapper(self.gitlab).get_gitlab()
|
|
23
|
+
|
|
24
|
+
@configuration_to_safe_dict
|
|
25
|
+
def process(
|
|
26
|
+
self,
|
|
27
|
+
project_or_project_and_group: str,
|
|
28
|
+
configuration,
|
|
29
|
+
dry_run: bool,
|
|
30
|
+
diff_only_changed: bool,
|
|
31
|
+
effective_configuration: EffectiveConfigurationFile,
|
|
32
|
+
):
|
|
33
|
+
if self._section_is_in_config(configuration):
|
|
34
|
+
if configuration.get(f"{self.configuration_name}|skip"):
|
|
35
|
+
info(f"Skipping section '{self.configuration_name}' - explicitly configured to do so.")
|
|
36
|
+
return
|
|
37
|
+
elif configuration.get("project|archive") and self.configuration_name != "project":
|
|
38
|
+
info(f"Skipping section '{self.configuration_name}' - it is configured to be archived.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if dry_run:
|
|
42
|
+
info(f"Processing section '{self.configuration_name}' in dry-run mode.")
|
|
43
|
+
project_transfer_source = ""
|
|
44
|
+
try:
|
|
45
|
+
project_transfer_source = configuration["project"]["transfer_from"]
|
|
46
|
+
info(f"""Project {project_or_project_and_group} is configured to be transferred,
|
|
47
|
+
diffing config from transfer source project {project_transfer_source}.""")
|
|
48
|
+
except KeyError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
self._print_diff(
|
|
52
|
+
project_transfer_source or project_or_project_and_group,
|
|
53
|
+
configuration.get(self.configuration_name),
|
|
54
|
+
diff_only_changed=diff_only_changed,
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
info(f"Processing section '{self.configuration_name}'")
|
|
58
|
+
if self._can_proceed(project_or_project_and_group, configuration):
|
|
59
|
+
self._process_configuration_with_retries(project_or_project_and_group, configuration)
|
|
60
|
+
|
|
61
|
+
effective_configuration.add_configuration(
|
|
62
|
+
project_or_project_and_group,
|
|
63
|
+
self.configuration_name,
|
|
64
|
+
configuration.get(self.configuration_name),
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
info(f"Skipping section '{self.configuration_name}' - not in config.")
|
|
68
|
+
|
|
69
|
+
def _section_is_in_config(self, configuration: dict):
|
|
70
|
+
return self.configuration_name in configuration
|
|
71
|
+
|
|
72
|
+
def _process_configuration_with_retries(self, project_or_project_and_group: str, configuration: dict):
|
|
73
|
+
retry = 1
|
|
74
|
+
max_retries = 3
|
|
75
|
+
|
|
76
|
+
while True:
|
|
77
|
+
try:
|
|
78
|
+
if retry > 1:
|
|
79
|
+
info(f"Retrying section '{self.configuration_name}' - {retry}/{max_retries}...")
|
|
80
|
+
|
|
81
|
+
self._process_configuration(
|
|
82
|
+
project_or_project_and_group,
|
|
83
|
+
configuration,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return
|
|
87
|
+
except Exception as e:
|
|
88
|
+
if retry > max_retries:
|
|
89
|
+
raise MaxProcessorRetriesExceeded from e
|
|
90
|
+
|
|
91
|
+
if self._should_retry_processor(e):
|
|
92
|
+
retry += 1
|
|
93
|
+
continue
|
|
94
|
+
else:
|
|
95
|
+
raise e
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _should_retry_processor(e: Exception) -> bool:
|
|
99
|
+
# Most possible failures during processing are handled by the HTTP request retries in GitLabCore class,
|
|
100
|
+
# but in some cases we cannot do that on that level.
|
|
101
|
+
|
|
102
|
+
# If we already retried on a request level, don't retry again
|
|
103
|
+
if "Max retries exceeded with url" in str(e):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# One case is when a POST request is made and the request is sent, but we got no (or incomplete?) response.
|
|
107
|
+
# Because we don't know if the particular POST request was done under the hood (f.e. an entity was created),
|
|
108
|
+
# we cannot retry just the single request (f.e. if it was created then a retry would either create a duplicate
|
|
109
|
+
# of the entity or fail with an error, if duplicates are not allowed in a given case).
|
|
110
|
+
|
|
111
|
+
# Then we need to retry the whole section (f.e. files) for a given entity (f.e. project foo/bar), so the checks
|
|
112
|
+
# for the initial state will re-run (f.e. checking if that entity already exists or not).
|
|
113
|
+
|
|
114
|
+
# fmt: off
|
|
115
|
+
if type(e) == requests.exceptions.ConnectionError \
|
|
116
|
+
and "RemoteDisconnected('Remote end closed connection without response')" in str(e):
|
|
117
|
+
return True
|
|
118
|
+
# fmt: on
|
|
119
|
+
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def _process_configuration(self, project_or_project_and_group: str, configuration: dict):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def _print_diff(self, project_or_project_and_group: str, entity_config, diff_only_changed: bool):
|
|
127
|
+
info(f"Diffing for section '{self.configuration_name}' is not supported yet")
|
|
128
|
+
|
|
129
|
+
def _needs_update(
|
|
130
|
+
self,
|
|
131
|
+
entity_in_gitlab: dict,
|
|
132
|
+
entity_in_configuration: dict,
|
|
133
|
+
):
|
|
134
|
+
# in the configuration we often don't define every key value because we rely on the defaults.
|
|
135
|
+
# that's why GitLab API often returns many more keys than we have in the configuration.
|
|
136
|
+
|
|
137
|
+
# so to decide if the entity should be updated:
|
|
138
|
+
# a) we look for ANY settings that are ONLY in configuration,
|
|
139
|
+
# a) we compare the settings that are in both configuration and gitlab,
|
|
140
|
+
|
|
141
|
+
keys_only_in_configuration = set(entity_in_configuration.keys()) - set(entity_in_gitlab.keys())
|
|
142
|
+
if len(keys_only_in_configuration) > 0:
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
keys_on_both_sides = set(entity_in_configuration.keys()) & set(entity_in_gitlab.keys())
|
|
146
|
+
for key in keys_on_both_sides:
|
|
147
|
+
if key in self.custom_diff_analyzers:
|
|
148
|
+
return self.custom_diff_analyzers[key](key, entity_in_gitlab[key], entity_in_configuration[key])
|
|
149
|
+
|
|
150
|
+
if entity_in_gitlab[key] != entity_in_configuration[key]:
|
|
151
|
+
debug(
|
|
152
|
+
f"entity_in_gitlab[{key}] -> {entity_in_gitlab[key]} != entity_in_configuration[{key}] -> {entity_in_configuration[key]}"
|
|
153
|
+
)
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def recursive_diff_analyzer(cfg_key: str, cfg_in_gitlab: list, local_cfg: list):
|
|
160
|
+
"""
|
|
161
|
+
:return: True if the lists are NOT equal, False otherwise
|
|
162
|
+
"""
|
|
163
|
+
if len(cfg_in_gitlab) != len(local_cfg):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
for index in range(len(cfg_in_gitlab)):
|
|
167
|
+
from_gitlab = {k: v for k, v in cfg_in_gitlab[index].items() if v is not None}
|
|
168
|
+
from_local_cfg = local_cfg[index]
|
|
169
|
+
|
|
170
|
+
keys_on_both_sides = set(from_gitlab.keys()) & set(from_local_cfg.keys())
|
|
171
|
+
|
|
172
|
+
for key in keys_on_both_sides:
|
|
173
|
+
if isinstance(from_gitlab[key], list) and isinstance(from_local_cfg[key], list):
|
|
174
|
+
AbstractProcessor.recursive_diff_analyzer(key, from_gitlab[key], from_local_cfg[key])
|
|
175
|
+
|
|
176
|
+
if from_gitlab[key] != from_local_cfg[key]:
|
|
177
|
+
debug(f"* A <{key}> in [{cfg_key}] differs:\n GitLab :: {from_gitlab} != Local :: {from_local_cfg}")
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def _can_proceed(self, project_or_group: str, configuration: dict):
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class MaxProcessorRetriesExceeded(Exception):
|
|
187
|
+
pass
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from gitlabform.configuration import Configuration
|
|
4
|
+
from gitlabform.gitlab import GitLab
|
|
5
|
+
from gitlabform.processors import AbstractProcessors
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
from gitlabform.processors.application.application_settings_processor import (
|
|
8
|
+
ApplicationSettingsProcessor,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ApplicationProcessors(AbstractProcessors):
|
|
13
|
+
def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
|
|
14
|
+
super().__init__(gitlab, config, strict)
|
|
15
|
+
self.processors: List[AbstractProcessor] = [
|
|
16
|
+
ApplicationSettingsProcessor(gitlab),
|
|
17
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from logging import info, debug
|
|
2
|
+
|
|
3
|
+
from gitlab.v4.objects import ApplicationSettings
|
|
4
|
+
|
|
5
|
+
from gitlabform import GitLab
|
|
6
|
+
from gitlabform.processors import AbstractProcessor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# https://docs.gitlab.com/ee/api/settings.html
|
|
10
|
+
class ApplicationSettingsProcessor(AbstractProcessor):
|
|
11
|
+
|
|
12
|
+
def __init__(self, gitlab: GitLab):
|
|
13
|
+
super().__init__("settings", gitlab)
|
|
14
|
+
|
|
15
|
+
def _process_configuration(self, project_or_group_name: str, application_configuration: dict):
|
|
16
|
+
application_settings_config = application_configuration["settings"]
|
|
17
|
+
application_settings: ApplicationSettings = self.gl.settings.get()
|
|
18
|
+
|
|
19
|
+
if self._needs_update(application_settings.asdict(), application_settings_config):
|
|
20
|
+
info("Updating Application Settings")
|
|
21
|
+
self.update_application_settings(application_settings, application_settings_config)
|
|
22
|
+
else:
|
|
23
|
+
debug("No update needed for Application Settings")
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def update_application_settings(application_settings: ApplicationSettings, application_settings_config: dict):
|
|
27
|
+
# application settings has to be like this:
|
|
28
|
+
# {
|
|
29
|
+
# 'setting1': value1,
|
|
30
|
+
# 'setting2': value2,
|
|
31
|
+
# }
|
|
32
|
+
# ..as documented at: https://docs.gitlab.com/ee/api/settings.html#change-application-settings
|
|
33
|
+
|
|
34
|
+
for key in application_settings_config:
|
|
35
|
+
value = application_settings_config[key]
|
|
36
|
+
debug(f"Updating setting {key} to value {value}")
|
|
37
|
+
application_settings.__setattr__(key, value)
|
|
38
|
+
|
|
39
|
+
application_settings.save()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AbstractKey(ABC):
|
|
5
|
+
"""
|
|
6
|
+
This represents a key in an entity configuration.
|
|
7
|
+
|
|
8
|
+
For example in such an entity:
|
|
9
|
+
|
|
10
|
+
rule:
|
|
11
|
+
name: foo
|
|
12
|
+
rule_type: bar
|
|
13
|
+
|
|
14
|
+
...the "name" and "rule_type" are keys.
|
|
15
|
+
|
|
16
|
+
This may also be an expression made with keys and relationships between them, see below.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def matches(self, e1, e2):
|
|
21
|
+
"""
|
|
22
|
+
:param e1: some entity
|
|
23
|
+
:param e2: another entity
|
|
24
|
+
:return: True if the two given entities have the same value of this key.
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def contains(self, entity):
|
|
30
|
+
"""
|
|
31
|
+
:param entity: some entity
|
|
32
|
+
:return: True if the entity contains this key
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def explain(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
:return: a user-friendly explanation of what this key is
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Key(AbstractKey):
|
|
45
|
+
"""
|
|
46
|
+
A single, mandatory key.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, name):
|
|
50
|
+
self.name = name
|
|
51
|
+
|
|
52
|
+
def matches(self, e1, e2):
|
|
53
|
+
return self.name in e1 and self.name in e2 and e1[self.name] == e2[self.name]
|
|
54
|
+
|
|
55
|
+
def contains(self, entity):
|
|
56
|
+
return entity.get(self.name, None) is not None
|
|
57
|
+
|
|
58
|
+
def explain(self) -> str:
|
|
59
|
+
return f"'{self.name}'"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class And(AbstractKey):
|
|
63
|
+
"""
|
|
64
|
+
This groups two or more keys of which all are mandatory.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, *arg: AbstractKey):
|
|
68
|
+
self.keys = arg
|
|
69
|
+
|
|
70
|
+
def matches(self, e1, e2):
|
|
71
|
+
return all([key.matches(e1, e2) for key in self.keys])
|
|
72
|
+
|
|
73
|
+
def contains(self, entity):
|
|
74
|
+
return all([key.contains(entity) for key in self.keys])
|
|
75
|
+
|
|
76
|
+
def explain(self) -> str:
|
|
77
|
+
explains = [key.explain() for key in self.keys]
|
|
78
|
+
return f"({' and '.join(explains)})"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Or(AbstractKey):
|
|
82
|
+
"""
|
|
83
|
+
This groups two or more keys where at least one must exist.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *arg: AbstractKey):
|
|
87
|
+
self.keys = arg
|
|
88
|
+
|
|
89
|
+
def matches(self, e1, e2):
|
|
90
|
+
return any([key.matches(e1, e2) for key in self.keys])
|
|
91
|
+
|
|
92
|
+
def contains(self, entity):
|
|
93
|
+
return any([key.contains(entity) for key in self.keys])
|
|
94
|
+
|
|
95
|
+
def explain(self) -> str:
|
|
96
|
+
explains = [key.explain() for key in self.keys]
|
|
97
|
+
return f"({' or '.join(explains)})"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Xor(AbstractKey):
|
|
101
|
+
"""
|
|
102
|
+
This groups two or more keys where exactly one must exist.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, *arg: AbstractKey):
|
|
106
|
+
self.keys = arg
|
|
107
|
+
|
|
108
|
+
# copied from https://stackoverflow.com/a/16801336/2693875
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _single_true(iterable):
|
|
111
|
+
iterator = iter(iterable)
|
|
112
|
+
|
|
113
|
+
# consume from "i" until first true, or it's exhausted
|
|
114
|
+
has_true = any(iterator)
|
|
115
|
+
|
|
116
|
+
# carry on consuming until another true value / exhausted
|
|
117
|
+
has_another_true = any(iterator)
|
|
118
|
+
|
|
119
|
+
# True if exactly one true found
|
|
120
|
+
return has_true and not has_another_true
|
|
121
|
+
|
|
122
|
+
def matches(self, e1, e2):
|
|
123
|
+
return self._single_true([key.matches(e1, e2) for key in self.keys])
|
|
124
|
+
|
|
125
|
+
def contains(self, entity):
|
|
126
|
+
return self._single_true([key.contains(entity) for key in self.keys])
|
|
127
|
+
|
|
128
|
+
def explain(self) -> str:
|
|
129
|
+
explains = [key.explain() for key in self.keys]
|
|
130
|
+
return f"(exactly one of: {', '.join(explains)})"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class OptionalKey(AbstractKey):
|
|
134
|
+
"""
|
|
135
|
+
This is a non-mandatory key.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, name):
|
|
139
|
+
self.name = name
|
|
140
|
+
|
|
141
|
+
def matches(self, e1, e2):
|
|
142
|
+
only_in_e1 = self.name in e1 and self.name not in e2
|
|
143
|
+
only_in_e2 = self.name not in e1 and self.name in e2
|
|
144
|
+
in_both_and_equal = self.name in e1 and self.name in e2 and e1[self.name] == e2[self.name]
|
|
145
|
+
return only_in_e1 or only_in_e2 or in_both_and_equal
|
|
146
|
+
|
|
147
|
+
def contains(self, entity):
|
|
148
|
+
# as it's optional, we don't check for its existence
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def explain(self) -> str:
|
|
152
|
+
return f"(optionally '{self.name}')"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from gitlabform.configuration import Configuration
|
|
4
|
+
from gitlabform.gitlab import GitLab
|
|
5
|
+
from gitlabform.processors import AbstractProcessors
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
from gitlabform.processors.group.group_badges_processor import GroupBadgesProcessor
|
|
8
|
+
from gitlabform.processors.group.group_ldap_links_processor import (
|
|
9
|
+
GroupLDAPLinksProcessor,
|
|
10
|
+
)
|
|
11
|
+
from gitlabform.processors.group.group_members_processor import (
|
|
12
|
+
GroupMembersProcessor,
|
|
13
|
+
)
|
|
14
|
+
from gitlabform.processors.group.group_saml_links_processor import (
|
|
15
|
+
GroupSAMLLinksProcessor,
|
|
16
|
+
)
|
|
17
|
+
from gitlabform.processors.group.group_variables_processor import (
|
|
18
|
+
GroupVariablesProcessor,
|
|
19
|
+
)
|
|
20
|
+
from gitlabform.processors.group.group_settings_processor import (
|
|
21
|
+
GroupSettingsProcessor,
|
|
22
|
+
)
|
|
23
|
+
from gitlabform.processors.group.group_labels_processor import (
|
|
24
|
+
GroupLabelsProcessor,
|
|
25
|
+
)
|
|
26
|
+
from gitlabform.processors.group.group_push_rules_processor import (
|
|
27
|
+
GroupPushRulesProcessor,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from gitlabform.processors.group.group_hooks_processor import (
|
|
31
|
+
GroupHooksProcessor,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GroupProcessors(AbstractProcessors):
|
|
36
|
+
def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
|
|
37
|
+
super().__init__(gitlab, config, strict)
|
|
38
|
+
self.processors: List[AbstractProcessor] = [
|
|
39
|
+
GroupVariablesProcessor(gitlab),
|
|
40
|
+
GroupSettingsProcessor(gitlab),
|
|
41
|
+
GroupMembersProcessor(gitlab),
|
|
42
|
+
GroupLDAPLinksProcessor(gitlab),
|
|
43
|
+
GroupBadgesProcessor(gitlab),
|
|
44
|
+
GroupSAMLLinksProcessor(gitlab),
|
|
45
|
+
GroupLabelsProcessor(gitlab),
|
|
46
|
+
GroupHooksProcessor(gitlab),
|
|
47
|
+
GroupPushRulesProcessor(gitlab),
|
|
48
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from gitlabform.gitlab import GitLab
|
|
2
|
+
from gitlabform.processors.defining_keys import Key, And
|
|
3
|
+
from gitlabform.processors.multiple_entities_processor import MultipleEntitiesProcessor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GroupBadgesProcessor(MultipleEntitiesProcessor):
|
|
7
|
+
def __init__(self, gitlab: GitLab):
|
|
8
|
+
super().__init__(
|
|
9
|
+
"group_badges",
|
|
10
|
+
gitlab,
|
|
11
|
+
list_method_name="get_group_badges",
|
|
12
|
+
add_method_name="add_group_badge",
|
|
13
|
+
delete_method_name="delete_group_badge",
|
|
14
|
+
defining=Key("name"),
|
|
15
|
+
required_to_create_or_update=And(Key("name"), Key("link_url"), Key("image_url")),
|
|
16
|
+
edit_method_name="edit_group_badge",
|
|
17
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from logging import debug, warning
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
from gitlab.base import RESTObject, RESTObjectList
|
|
5
|
+
from gitlab.v4.objects import Group
|
|
6
|
+
from gitlab.v4.objects import GroupHook
|
|
7
|
+
|
|
8
|
+
from gitlabform.gitlab import GitLab
|
|
9
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GroupHooksProcessor(AbstractProcessor):
|
|
13
|
+
def __init__(self, gitlab: GitLab):
|
|
14
|
+
super().__init__("group_hooks", gitlab)
|
|
15
|
+
|
|
16
|
+
def _can_proceed(self, project_or_group: str, configuration: dict):
|
|
17
|
+
if not self.gitlab.enterprise:
|
|
18
|
+
hooks_in_config = configuration["group_hooks"]
|
|
19
|
+
if hooks_in_config is not None and len(hooks_in_config) > 0:
|
|
20
|
+
# Only raise error if user has defined hooks in config, otherwise exit silently out of processor
|
|
21
|
+
warning("GitLab Community Edition does not support Group Webhooks")
|
|
22
|
+
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
def _process_configuration(self, group_path_and_name: str, configuration: dict):
|
|
28
|
+
hooks_in_config: tuple[str, ...] = tuple(x for x in sorted(configuration["group_hooks"]) if x != "enforce")
|
|
29
|
+
|
|
30
|
+
debug("Processing group hooks...")
|
|
31
|
+
group: Group = self.gl.get_group_by_path_cached(group_path_and_name)
|
|
32
|
+
group_hooks: list[GroupHook] = group.hooks.list(get_all=True)
|
|
33
|
+
|
|
34
|
+
for hook in hooks_in_config:
|
|
35
|
+
hook_in_gitlab: RESTObject | None = next((h for h in group_hooks if h.url == hook), None)
|
|
36
|
+
hook_config = {"url": hook}
|
|
37
|
+
hook_config.update(configuration["group_hooks"][hook])
|
|
38
|
+
|
|
39
|
+
hook_id = hook_in_gitlab.id if hook_in_gitlab else None
|
|
40
|
+
|
|
41
|
+
# Process hooks configured for deletion
|
|
42
|
+
if configuration.get("group_hooks|" + hook + "|delete"):
|
|
43
|
+
if hook_id:
|
|
44
|
+
debug(f"Deleting group hook '{hook}'")
|
|
45
|
+
group.hooks.delete(hook_id)
|
|
46
|
+
debug(f"Deleted group hook '{hook}'")
|
|
47
|
+
else:
|
|
48
|
+
debug(f"Not deleting group hook '{hook}', because it doesn't exist")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# Process new hook creation
|
|
52
|
+
if not hook_id:
|
|
53
|
+
debug(f"Creating group hook '{hook}'")
|
|
54
|
+
created_hook: RESTObject = group.hooks.create(hook_config)
|
|
55
|
+
debug(f"Created group hook: {created_hook}")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Processing existing hook updates
|
|
59
|
+
gl_hook: dict = hook_in_gitlab.asdict() if hook_in_gitlab else {}
|
|
60
|
+
if self._needs_update(gl_hook, hook_config):
|
|
61
|
+
debug(f"The group hook '{hook}' config is different from what's in gitlab OR it contains a token")
|
|
62
|
+
debug(f"Updating group hook '{hook}'")
|
|
63
|
+
updated_hook: Dict[str, Any] = group.hooks.update(hook_id, hook_config)
|
|
64
|
+
debug(f"Updated group hook: {updated_hook}")
|
|
65
|
+
else:
|
|
66
|
+
debug(f"Group hook '{hook}' remains unchanged")
|
|
67
|
+
|
|
68
|
+
# Process hook config enforcements
|
|
69
|
+
if configuration.get("group_hooks|enforce"):
|
|
70
|
+
for gh in group_hooks:
|
|
71
|
+
if gh.url not in hooks_in_config:
|
|
72
|
+
debug(
|
|
73
|
+
f"Deleting group hook '{gh.url}' currently setup in the group but it is not in the configuration and enforce is enabled"
|
|
74
|
+
)
|
|
75
|
+
group.hooks.delete(gh.id)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from logging import info
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from gitlabform.gitlab import GitLab
|
|
6
|
+
from gitlabform.processors.abstract_processor import AbstractProcessor
|
|
7
|
+
from gitlabform.processors.util.labels_processor import LabelsProcessor
|
|
8
|
+
|
|
9
|
+
from gitlab.v4.objects import Group
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GroupLabelsProcessor(AbstractProcessor):
|
|
13
|
+
def __init__(self, gitlab: GitLab):
|
|
14
|
+
super().__init__("group_labels", gitlab)
|
|
15
|
+
self._labels_processor = LabelsProcessor()
|
|
16
|
+
|
|
17
|
+
def _process_configuration(self, group_path_and_name: str, configuration: Dict):
|
|
18
|
+
configured_labels = configuration.get("group_labels", {})
|
|
19
|
+
|
|
20
|
+
enforce = configuration.get("group_labels|enforce", False)
|
|
21
|
+
|
|
22
|
+
# Remove 'enforce' key from the config so that it's not treated as a "label"
|
|
23
|
+
if enforce:
|
|
24
|
+
configured_labels.pop("enforce")
|
|
25
|
+
|
|
26
|
+
group: Group = self.gl.get_group_by_path_cached(group_path_and_name)
|
|
27
|
+
|
|
28
|
+
self._labels_processor.process_labels(configured_labels, enforce, group, self._needs_update)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from gitlabform.gitlab import GitLab
|
|
2
|
+
from gitlabform.processors.defining_keys import And, Or, Key, Xor
|
|
3
|
+
from gitlabform.processors.multiple_entities_processor import MultipleEntitiesProcessor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GroupLDAPLinksProcessor(MultipleEntitiesProcessor):
|
|
7
|
+
def __init__(self, gitlab: GitLab):
|
|
8
|
+
super().__init__(
|
|
9
|
+
"group_ldap_links",
|
|
10
|
+
gitlab,
|
|
11
|
+
list_method_name="get_ldap_group_links",
|
|
12
|
+
add_method_name="add_ldap_group_link",
|
|
13
|
+
delete_method_name="delete_ldap_group_link",
|
|
14
|
+
defining=And(Key("provider"), Or(Key("cn"), Key("filter"))),
|
|
15
|
+
required_to_create_or_update=And(Key("provider"), Xor(Key("cn"), Key("filter"))),
|
|
16
|
+
)
|