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,259 @@
1
+ import sys
2
+ from logging import debug, info, critical
3
+ from abc import ABC, abstractmethod
4
+ from ez_yaml import ez_yaml
5
+ from ruamel.yaml import YAML
6
+ from types import SimpleNamespace
7
+
8
+ from ruamel.yaml.comments import CommentedMap
9
+ from yamlpath import Processor
10
+ from yamlpath.exceptions import YAMLPathException
11
+ from yamlpath.wrappers import ConsolePrinter
12
+
13
+ from gitlabform.constants import EXIT_INVALID_INPUT, APPROVAL_RULE_NAME
14
+ from gitlabform.configuration import Configuration
15
+ from gitlabform.gitlab import AccessLevel
16
+ from gitlabform.gitlab import GitLab
17
+
18
+ # Configuration transformers are classes which take the input configuration as YAML and change it
19
+ # to from the more user-friendly input to an output that is more applicable to passing to GitLab
20
+ # over its API.
21
+ #
22
+ # For example, we want to operate on usernames in the configuration while GitLab sometimes operates
23
+ # on user ids. Therefore, one of the transformers changes "user_id: <number>" into "user: <username>".
24
+
25
+
26
+ class ConfigurationTransformers:
27
+ def __init__(self, gitlab: GitLab):
28
+ self.user_transformer = UserTransformer(gitlab)
29
+ self.group_transformer = GroupTransformer(gitlab)
30
+ self.implicit_name_transformer = ImplicitNameTransformer(gitlab)
31
+ self.access_level_transformer = AccessLevelsTransformer(gitlab)
32
+
33
+ def transform(self, configuration: Configuration) -> None:
34
+ config_before = ez_yaml.to_string(obj=configuration.config, options={})
35
+ debug(f"Config BEFORE transformations:\n{config_before}")
36
+
37
+ self.user_transformer.transform(configuration)
38
+ self.group_transformer.transform(configuration)
39
+ self.implicit_name_transformer.transform(configuration)
40
+ self.access_level_transformer.transform(configuration, last=True)
41
+
42
+ config_after = ez_yaml.to_string(obj=configuration.config, options={})
43
+ debug(f"Config AFTER transformations:\n{config_after}")
44
+
45
+
46
+ class ConfigurationTransformer(ABC):
47
+ def transform(self, configuration: Configuration, last: bool = False) -> None:
48
+ self._do_transform(configuration)
49
+ if last:
50
+ self.convert_to_simple_types(configuration)
51
+
52
+ @abstractmethod
53
+ def _do_transform(self, configuration: Configuration) -> None:
54
+ pass
55
+
56
+ @staticmethod
57
+ def convert_to_simple_types(configuration: Configuration):
58
+ # we needed complex ruamel.yaml's types like ordereddict and CommentedSeq
59
+ # for transformations, but at the end convert them to simple dict and lists
60
+ # for easier to understand debug output and tests
61
+
62
+ config_yaml_string = ez_yaml.to_string(obj=configuration.config, options={})
63
+ simple_yaml_loader = YAML(typ="safe", pure=True)
64
+ configuration.config = simple_yaml_loader.load(config_yaml_string)
65
+
66
+
67
+ class UserTransformer(ConfigurationTransformer):
68
+ def __init__(self, gitlab: GitLab):
69
+ self.gitlab = gitlab
70
+
71
+ def _do_transform(self, configuration: Configuration) -> None:
72
+ logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False)
73
+ log = ConsolePrinter(logging_args)
74
+ processor = Processor(log, configuration.config)
75
+ info("Getting user ids for users defined in protect_environments config")
76
+ try:
77
+ for node_coordinate in processor.get_nodes(
78
+ "projects_and_groups.*.protected_environments.*.deploy_access_levels.user",
79
+ mustexist=True,
80
+ ):
81
+ user = node_coordinate.parent.pop("user")
82
+
83
+ node_coordinate.parent["user_id"] = self.gitlab._get_user_id(user)
84
+ except YAMLPathException as e:
85
+ # this just means that we haven't found any keys in YAML
86
+ # under the given path
87
+ pass
88
+
89
+ info("Getting user ids for users defined in merge_requests_approval_rules config")
90
+ try:
91
+ for node_coordinate in processor.get_nodes(
92
+ "**.merge_requests_approval_rules.*.users",
93
+ mustexist=True,
94
+ ):
95
+ user_ids = []
96
+ users = node_coordinate.parent.pop("users")
97
+ for user in users:
98
+ user_id = self.gitlab._get_user_id(user)
99
+ user_ids.append(user_id)
100
+ node_coordinate.parent["user_ids"] = user_ids
101
+ except YAMLPathException as e:
102
+ # this just means that we haven't found any keys in YAML
103
+ # under the given path
104
+ pass
105
+
106
+
107
+ class GroupTransformer(ConfigurationTransformer):
108
+ def __init__(self, gitlab: GitLab):
109
+ self.gitlab = gitlab
110
+
111
+ def _do_transform(self, configuration: Configuration) -> None:
112
+ logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False)
113
+ processor = Processor(ConsolePrinter(logging_args), configuration.config)
114
+
115
+ try:
116
+ for node_coordinate in processor.get_nodes(
117
+ "projects_and_groups.*.protected_environments.*.deploy_access_levels.group",
118
+ mustexist=True,
119
+ ):
120
+ group = node_coordinate.parent.pop("group")
121
+ node_coordinate.parent["group_id"] = self.gitlab._get_group_id(group)
122
+
123
+ except YAMLPathException as e:
124
+ # this just means that we haven't found any keys in YAML
125
+ # under the given path
126
+ pass
127
+
128
+ try:
129
+ for node_coordinate in processor.get_nodes(
130
+ "**.merge_requests_approval_rules.*.groups",
131
+ mustexist=True,
132
+ ):
133
+ group_ids = []
134
+ groups = node_coordinate.parent.pop("groups")
135
+ for group in groups:
136
+ group_id = self.gitlab._get_group_id(group)
137
+ group_ids.append(group_id)
138
+ node_coordinate.parent["group_ids"] = group_ids
139
+ except YAMLPathException:
140
+ # this just means that we haven't found any keys in YAML
141
+ # under the given path
142
+ pass
143
+
144
+
145
+ class ImplicitNameTransformer(ConfigurationTransformer):
146
+ """
147
+ Creates a 'name' field that has the same value as the "scope" delimiter, e.g.:
148
+
149
+ ...
150
+ blah: # start of the cfg scope
151
+ name: blah # name to be used
152
+ smth_else: <...>
153
+
154
+ It's redundant, so this can be done as :
155
+
156
+ ...
157
+ foo: # a 'name' field will be created as -> name: foo
158
+ smth_else: <...>
159
+ """
160
+
161
+ def __init__(self, gitlab: GitLab):
162
+ # this transformer doesn't need to call gitlab
163
+ pass
164
+
165
+ def _do_transform(self, configuration: Configuration) -> None:
166
+ logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False)
167
+ processor = Processor(ConsolePrinter(logging_args), configuration.config)
168
+
169
+ try:
170
+ for node_coordinate in processor.get_nodes(
171
+ "projects_and_groups.*.protected_environments.*",
172
+ mustexist=True,
173
+ ):
174
+ if type(node_coordinate.node) not in [CommentedMap, dict]:
175
+ continue
176
+
177
+ node_coordinate.parent[node_coordinate.parentref]["name"] = node_coordinate.parentref
178
+ except YAMLPathException:
179
+ # this just means that we haven't found any keys in YAML
180
+ # under the given path
181
+ pass
182
+
183
+
184
+ class AccessLevelsTransformer(ConfigurationTransformer):
185
+ """
186
+ Internally the app supports only numeric access levels, but for user-friendliness
187
+ this class allows providing them as strings and transforms these strings into
188
+ the appropriate numbers.
189
+ """
190
+
191
+ def __init__(self, gitlab: GitLab):
192
+ # this transformer doesn't need to call gitlab
193
+ pass
194
+
195
+ def _do_transform(self, configuration: Configuration):
196
+ logging_args = SimpleNamespace(quiet=False, verbose=False, debug=False)
197
+ processor = Processor(ConsolePrinter(logging_args), configuration.config)
198
+
199
+ # [.!<100] effectively means that the value is non-numerical
200
+ paths_to_hashes = [
201
+ # # branches, old syntax
202
+ "**.push_access_level[.!<100]",
203
+ "**.merge_access_level[.!<100]",
204
+ "**.unprotect_access_level[.!<100]",
205
+ # members & group members
206
+ "**.access_level[.!<100]",
207
+ "**.group_access[.!<100]",
208
+ # old syntax
209
+ "**.group_access_level[.!<100]",
210
+ # tags
211
+ "**.create_access_level[.!<100]",
212
+ ]
213
+
214
+ for path in paths_to_hashes:
215
+ try:
216
+ for node_coordinate in processor.get_nodes(path, mustexist=True):
217
+ try:
218
+ access_level_string = str(node_coordinate.node)
219
+ node_coordinate.parent[node_coordinate.parentref] = AccessLevel.get_value(access_level_string)
220
+ except KeyError:
221
+ critical(
222
+ f"Configuration string '{access_level_string}' is not one of the valid access levels:"
223
+ f" {', '.join(AccessLevel.get_canonical_names())}"
224
+ )
225
+ sys.exit(EXIT_INVALID_INPUT)
226
+ except YAMLPathException:
227
+ # this just means that we haven't found any keys in YAML
228
+ # under the given path
229
+ pass
230
+
231
+ # these are different from the above, as they are elements of arrays,
232
+ # so we need different search query and an extra condition for
233
+ # transformation
234
+ paths_to_arrays = [
235
+ # # branches, new GitLab Premium syntax
236
+ "**.allowed_to_push.*.[access_level!<100]",
237
+ "**.allowed_to_merge.*.[access_level!<100]",
238
+ "**.allowed_to_unprotect.*.[access_level!<100]",
239
+ ]
240
+
241
+ for path in paths_to_arrays:
242
+ try:
243
+ for node_coordinate in processor.get_nodes(path, mustexist=True):
244
+ if node_coordinate.parentref == "access_level":
245
+ try:
246
+ access_level_string = str(node_coordinate.node)
247
+ node_coordinate.parent[node_coordinate.parentref] = AccessLevel.get_value(
248
+ access_level_string
249
+ )
250
+ except KeyError:
251
+ critical(
252
+ f"Configuration string '{access_level_string}' is not one of the valid access levels:"
253
+ f" {', '.join(AccessLevel.get_canonical_names())}"
254
+ )
255
+ sys.exit(EXIT_INVALID_INPUT)
256
+ except YAMLPathException:
257
+ # this just means that we haven't found any keys in YAML
258
+ # under the given path
259
+ pass
@@ -0,0 +1,7 @@
1
+ # f.e. bad syntax in the config file ~= "it's your fault" 😅
2
+ EXIT_INVALID_INPUT = 1
3
+ # f.e. when requests to GitLab fail ~= "it's not your fault" 😎
4
+ EXIT_PROCESSING_ERROR = 2
5
+
6
+ # legacy single approval rule name
7
+ APPROVAL_RULE_NAME = "Approvers (configured using GitLabForm)"
@@ -0,0 +1,108 @@
1
+ import enum
2
+ import inspect
3
+ import logging
4
+
5
+ from typing import List
6
+
7
+ from gitlab import Gitlab as GitlabClient, GraphQL
8
+
9
+ from gitlabform.gitlab.commits import GitLabCommits
10
+ from gitlabform.gitlab.group_badges import GitLabGroupBadges
11
+ from gitlabform.gitlab.group_ldap_links import GitLabGroupLDAPLinks
12
+ from gitlabform.gitlab.merge_requests import GitLabMergeRequests
13
+ from gitlabform.gitlab.pipelines import GitLabPipelines
14
+ from gitlabform.gitlab.project_badges import GitLabProjectBadges
15
+ from gitlabform.gitlab.project_deploy_keys import GitLabProjectDeployKeys
16
+ from gitlabform.gitlab.project_protected_environments import (
17
+ GitLabProjectProtectedEnvironments,
18
+ )
19
+ from gitlabform.gitlab.project_merge_requests_approvals import (
20
+ GitLabProjectMergeRequestsApprovals,
21
+ )
22
+ from gitlabform.gitlab.python_gitlab import PythonGitlab
23
+
24
+
25
+ @enum.unique
26
+ class AccessLevel(enum.IntEnum):
27
+ NO_ACCESS = 0
28
+ MINIMAL = 5 # introduced in GitLab 13.5
29
+ GUEST = 10
30
+ PLANNER = 15 # introduced in GitLab 17.7
31
+ REPORTER = 20
32
+ DEVELOPER = 30
33
+ MAINTAINER = 40
34
+ OWNER = 50 # only for groups
35
+ ADMIN = 60
36
+
37
+ @classmethod
38
+ def group_levels(cls) -> List[int]:
39
+ return [level.value for level in AccessLevel if level <= 50]
40
+
41
+ @classmethod
42
+ def get_value(cls, name: str) -> int:
43
+ # for the above set of key names this is enough for an effectively fuzzy name matching
44
+ return AccessLevel[name.strip().upper().replace(" ", "_")].value
45
+
46
+ @classmethod
47
+ def get_canonical_names(cls) -> List[str]:
48
+ return [level.name.lower().replace("_", " ") for level in AccessLevel]
49
+
50
+
51
+ class GitLab(
52
+ GitLabCommits,
53
+ GitLabMergeRequests,
54
+ GitLabGroupLDAPLinks,
55
+ GitLabGroupBadges,
56
+ GitLabPipelines,
57
+ GitLabProjectBadges,
58
+ GitLabProjectDeployKeys,
59
+ GitLabProjectProtectedEnvironments,
60
+ GitLabProjectMergeRequestsApprovals,
61
+ ):
62
+ pass
63
+
64
+
65
+ class GitlabWrapper:
66
+ # Parameters accepted by python-gitlab's Gitlab.__init__
67
+ # Other config keys (like max_retries) are used elsewhere in gitlabform
68
+ # or passed to specific components like GraphQL
69
+ GITLAB_CLIENT_PARAMS = set(inspect.signature(GitlabClient.__init__).parameters.keys()) - {"self"}
70
+
71
+ # Parameters accepted by python-gitlab's GraphQL.__init__
72
+ GRAPHQL_PARAMS = set(inspect.signature(GraphQL.__init__).parameters.keys()) - {"self"}
73
+
74
+ def __init__(self, gitlabform: GitLab):
75
+ session = gitlabform.session
76
+
77
+ graphql_kwargs = {k: v for k, v in gitlabform.gitlab_config.items() if k in self.GRAPHQL_PARAMS}
78
+ graphql = GraphQL(**graphql_kwargs)
79
+
80
+ default_gitlab_kwargs = {
81
+ "retry_transient_errors": True,
82
+ }
83
+ renamed_gitlab_kwargs = {
84
+ # Bandit is used for security scanning and it complains about 'private_token' being
85
+ # a hardcoded secret. However, in this case we are just renaming a config key
86
+ # provided by the user to match the parameter name expected by python-gitlab.
87
+ # Hence, we can safely ignore this code security warning here.
88
+ "token": "private_token", # nosec B105
89
+ }
90
+ extra_gitlab_kwargs = {
91
+ **default_gitlab_kwargs,
92
+ **{
93
+ k: v
94
+ for k, v in gitlabform.gitlab_config.items()
95
+ if k not in renamed_gitlab_kwargs and k in self.GITLAB_CLIENT_PARAMS
96
+ },
97
+ **{renamed_gitlab_kwargs[k]: v for k, v in gitlabform.gitlab_config.items() if k in renamed_gitlab_kwargs},
98
+ }
99
+
100
+ self._gitlab: PythonGitlab = PythonGitlab(
101
+ api_version="4",
102
+ graphql=graphql,
103
+ session=session,
104
+ **extra_gitlab_kwargs,
105
+ )
106
+
107
+ def get_gitlab(self):
108
+ return self._gitlab
@@ -0,0 +1,39 @@
1
+ from gitlabform.gitlab.core import GitLabCore
2
+
3
+
4
+ class GitLabCommits(GitLabCore):
5
+ def get_commit(self, project_and_group_name, sha):
6
+ return self._make_requests_to_api("projects/%s/repository/commits/%s", (project_and_group_name, sha))
7
+
8
+ def get_ahead_and_behind(self, project_and_group_name, protected_branch, feature_branch):
9
+ ahead = 0
10
+ behind = 0
11
+
12
+ response = self._make_requests_to_api(
13
+ "projects/%s/repository/compare?from=%s&to=%s",
14
+ (project_and_group_name, protected_branch, feature_branch),
15
+ )
16
+ if len(response) > 0:
17
+ ahead = len(response["commits"])
18
+
19
+ response = self._make_requests_to_api(
20
+ "projects/%s/repository/compare?from=%s&to=%s",
21
+ (project_and_group_name, feature_branch, protected_branch),
22
+ )
23
+ if len(response) > 0:
24
+ behind = len(response["commits"])
25
+
26
+ return ahead, behind
27
+
28
+ def get_last_commit(self, project_and_group_name, branch_name):
29
+ branch = self._make_requests_to_api("projects/%s/repository/branches/%s", (project_and_group_name, branch_name))
30
+ last_commit_hash = branch["commit"]["id"]
31
+ return self.get_commit(project_and_group_name, last_commit_hash)
32
+
33
+ def get_last_commit_attributes(self, project_and_group_name, branch):
34
+ commit = self.get_last_commit(project_and_group_name, branch)
35
+
36
+ # we want to read git's *commit date* instead of *author date*, so that "touching" the branch will invalidate
37
+ # its "getting older" counter
38
+
39
+ return commit["author_name"], commit["author_email"], commit["committed_date"]