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,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
+ )