saltext.github 1.1.0__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 (73) hide show
  1. saltext/github/__init__.py +31 -0
  2. saltext/github/modules/__init__.py +0 -0
  3. saltext/github/modules/github.py +1873 -0
  4. saltext/github/modules/github_org.py +667 -0
  5. saltext/github/modules/github_repo.py +1658 -0
  6. saltext/github/modules/github_user.py +75 -0
  7. saltext/github/resources/__init__.py +0 -0
  8. saltext/github/resources/github/__init__.py +251 -0
  9. saltext/github/resources/github/modules/__init__.py +0 -0
  10. saltext/github/resources/github/modules/actions.py +175 -0
  11. saltext/github/resources/github/modules/config.py +69 -0
  12. saltext/github/resources/github/modules/deploy_key.py +122 -0
  13. saltext/github/resources/github/modules/environment.py +241 -0
  14. saltext/github/resources/github/modules/grains.py +18 -0
  15. saltext/github/resources/github/modules/issue.py +157 -0
  16. saltext/github/resources/github/modules/label.py +161 -0
  17. saltext/github/resources/github/modules/milestone.py +103 -0
  18. saltext/github/resources/github/modules/pages.py +149 -0
  19. saltext/github/resources/github/modules/pull.py +90 -0
  20. saltext/github/resources/github/modules/repo.py +155 -0
  21. saltext/github/resources/github/modules/ruleset.py +144 -0
  22. saltext/github/resources/github/modules/saltutil.py +24 -0
  23. saltext/github/resources/github/modules/state.py +116 -0
  24. saltext/github/resources/github/modules/test.py +29 -0
  25. saltext/github/resources/github/states/__init__.py +0 -0
  26. saltext/github/resources/github/states/actions.py +117 -0
  27. saltext/github/resources/github/states/deploy_key.py +130 -0
  28. saltext/github/resources/github/states/environment.py +99 -0
  29. saltext/github/resources/github/states/label.py +71 -0
  30. saltext/github/resources/github/states/pages.py +92 -0
  31. saltext/github/resources/github/states/repo.py +108 -0
  32. saltext/github/resources/github/states/ruleset.py +81 -0
  33. saltext/github/states/__init__.py +0 -0
  34. saltext/github/states/github.py +966 -0
  35. saltext/github/states/github_org.py +369 -0
  36. saltext/github/states/github_repo.py +722 -0
  37. saltext/github/utils/__init__.py +72 -0
  38. saltext/github/utils/gh/__init__.py +14 -0
  39. saltext/github/utils/gh/actions.py +195 -0
  40. saltext/github/utils/gh/cache.py +126 -0
  41. saltext/github/utils/gh/client.py +534 -0
  42. saltext/github/utils/gh/deploy_keys.py +121 -0
  43. saltext/github/utils/gh/environments.py +271 -0
  44. saltext/github/utils/gh/exceptions.py +28 -0
  45. saltext/github/utils/gh/grains_files.py +366 -0
  46. saltext/github/utils/gh/issues.py +193 -0
  47. saltext/github/utils/gh/labels.py +142 -0
  48. saltext/github/utils/gh/members.py +157 -0
  49. saltext/github/utils/gh/milestones.py +122 -0
  50. saltext/github/utils/gh/pages.py +128 -0
  51. saltext/github/utils/gh/profile.py +458 -0
  52. saltext/github/utils/gh/pulls.py +99 -0
  53. saltext/github/utils/gh/repos.py +465 -0
  54. saltext/github/utils/gh/resource.py +35 -0
  55. saltext/github/utils/gh/rulesets.py +191 -0
  56. saltext/github/utils/gh/statecores/__init__.py +70 -0
  57. saltext/github/utils/gh/statecores/actions.py +180 -0
  58. saltext/github/utils/gh/statecores/deploy_keys.py +241 -0
  59. saltext/github/utils/gh/statecores/environments.py +212 -0
  60. saltext/github/utils/gh/statecores/labels.py +116 -0
  61. saltext/github/utils/gh/statecores/pages.py +177 -0
  62. saltext/github/utils/gh/statecores/repo.py +249 -0
  63. saltext/github/utils/gh/statecores/rulesets.py +115 -0
  64. saltext/github/utils/gh/teams.py +375 -0
  65. saltext/github/utils/gh/users.py +87 -0
  66. saltext/github/version.py +1 -0
  67. saltext_github-1.1.0.dist-info/METADATA +151 -0
  68. saltext_github-1.1.0.dist-info/RECORD +73 -0
  69. saltext_github-1.1.0.dist-info/WHEEL +5 -0
  70. saltext_github-1.1.0.dist-info/entry_points.txt +2 -0
  71. saltext_github-1.1.0.dist-info/licenses/LICENSE +201 -0
  72. saltext_github-1.1.0.dist-info/licenses/NOTICE +10 -0
  73. saltext_github-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,369 @@
1
+ """
2
+ Manage a GitHub organization: members, teams and organization rulesets.
3
+
4
+ .. versionadded:: 1.1.0
5
+ """
6
+
7
+ import logging
8
+
9
+ from salt.exceptions import CommandExecutionError
10
+
11
+ from saltext.github.utils import filter_unset
12
+ from saltext.github.utils.gh.exceptions import GitHubNotFoundError
13
+ from saltext.github.utils.gh.statecores import StateError
14
+ from saltext.github.utils.gh.statecores import catch_errors
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+ __virtualname__ = "github_org"
19
+
20
+
21
+ @catch_errors
22
+ def member_present(name, profile="github"):
23
+ """
24
+ Ensure a user is a member of the organization. Since GitHub
25
+ memberships require an invitation, this cannot be achieved in a
26
+ single run: Once the user has been invited, this state reports
27
+ success while the membership state is ``pending``.
28
+
29
+ name
30
+ GitHub login of the user.
31
+
32
+ profile
33
+ Profile configuration name to use. Defaults to ``github``.
34
+ """
35
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
36
+
37
+ try:
38
+ info = __salt__["github_org.get_member"](name, profile=profile)
39
+ except GitHubNotFoundError as err:
40
+ # astroid on Python 3.10 fails to infer `err` as an exception
41
+ # FIXME: Remove this once CI lint runs on a more recent Python version
42
+ raise CommandExecutionError( # pylint: disable=bad-exception-cause
43
+ f"User {name} does not exist"
44
+ ) from err
45
+
46
+ if info["in_org"]:
47
+ ret["comment"] = f"User {name} is already a member of the organization"
48
+ return ret
49
+ if info["membership_state"] == "pending":
50
+ ret["comment"] = f"User {name} has already been invited"
51
+ return ret
52
+
53
+ ret["changes"]["invited"] = name
54
+ if __opts__["test"]:
55
+ ret["result"] = None
56
+ ret["comment"] = f"User {name} would have been invited"
57
+ return ret
58
+
59
+ if __salt__["github_org.add_member"](name, profile=profile):
60
+ ret["comment"] = f"User {name} has been invited"
61
+ else:
62
+ ret["changes"] = {"added": name}
63
+ ret["comment"] = f"User {name} has been added"
64
+ return ret
65
+
66
+
67
+ @catch_errors
68
+ def member_absent(name, profile="github"):
69
+ """
70
+ Ensure a user is not a member of the organization.
71
+
72
+ name
73
+ GitHub login of the user.
74
+
75
+ profile
76
+ Profile configuration name to use. Defaults to ``github``.
77
+ """
78
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
79
+
80
+ try:
81
+ info = __salt__["github_org.get_member"](name, profile=profile)
82
+ except GitHubNotFoundError:
83
+ ret["comment"] = f"User {name} does not exist"
84
+ return ret
85
+
86
+ if not info["in_org"]:
87
+ ret["comment"] = f"User {name} is not a member of the organization"
88
+ return ret
89
+
90
+ ret["changes"]["removed"] = name
91
+ if __opts__["test"]:
92
+ ret["result"] = None
93
+ ret["comment"] = f"User {name} would have been removed from the organization"
94
+ return ret
95
+
96
+ __salt__["github_org.remove_member"](name, profile=profile)
97
+ ret["comment"] = f"User {name} has been removed from the organization"
98
+ return ret
99
+
100
+
101
+ def _case_insensitive_diff(current, desired):
102
+ """
103
+ Return (added, removed) between two collections of names,
104
+ compared case-insensitively since GitHub treats repository
105
+ names and user logins as such. ``added`` preserves the case
106
+ of the desired names, ``removed`` the current one.
107
+ """
108
+ current = {name.lower(): name for name in current}
109
+ desired = {name.lower(): name for name in desired}
110
+ added = sorted(desired[name] for name in set(desired) - set(current))
111
+ removed = sorted(current[name] for name in set(current) - set(desired))
112
+ return added, removed
113
+
114
+
115
+ @catch_errors
116
+ def team_present(
117
+ name,
118
+ description=None,
119
+ repo_names=None,
120
+ privacy=None,
121
+ permission=None,
122
+ members=None,
123
+ profile="github",
124
+ ):
125
+ """
126
+ Ensure a named team exists as specified.
127
+ Unspecified settings are left unmanaged.
128
+
129
+ .. note::
130
+
131
+ Unlike the deprecated :py:func:`github.team_present <saltext.github.states.github.team_present>`
132
+ state, this state does not implement per-team MFA enforcement.
133
+ Require 2FA for the whole organization in its GitHub settings instead.
134
+
135
+ name
136
+ Name of the team in the organization.
137
+
138
+ description
139
+ Team description.
140
+
141
+ repo_names
142
+ List of repository names the team should have access to.
143
+ Unlisted repositories are removed from the team.
144
+
145
+ privacy
146
+ Privacy level for the team, either ``secret`` or ``closed``.
147
+
148
+ permission
149
+ Default permission for new repositories added to the team, either
150
+ ``pull``, ``push`` or ``admin``.
151
+
152
+ members
153
+ Login handles of team members. Unlisted members are removed from the team.
154
+
155
+ profile
156
+ Profile configuration name to use. Defaults to ``github``.
157
+ """
158
+ ret = {
159
+ "name": name,
160
+ "changes": {},
161
+ "result": True,
162
+ "comment": f"Team {name} is present as specified",
163
+ }
164
+
165
+ try:
166
+ target = __salt__["github_org.get_team"](name, profile=profile)
167
+ except GitHubNotFoundError:
168
+ target = None
169
+
170
+ settings = filter_unset(
171
+ {"description": description, "privacy": privacy, "permission": permission}
172
+ )
173
+ repos_added = repos_removed = members_added = members_removed = []
174
+
175
+ if target is None:
176
+ ret["changes"]["created"] = name
177
+ repos_added = sorted(repo_names or [])
178
+ members_added = sorted(members or [])
179
+ else:
180
+ diff = {key: val for key, val in settings.items() if target[key] != val}
181
+ if diff:
182
+ ret["changes"]["old"] = {key: target[key] for key in diff}
183
+ ret["changes"]["new"] = diff
184
+ if repo_names is not None:
185
+ current_repos = __salt__["github_org.list_team_repos"](name, profile=profile)
186
+ repos_added, repos_removed = _case_insensitive_diff(current_repos, repo_names)
187
+ if members is not None:
188
+ current_members = __salt__["github_org.list_team_members"](name, profile=profile)
189
+ members_added, members_removed = _case_insensitive_diff(current_members, members)
190
+
191
+ for field, values in (
192
+ ("repos_added", repos_added),
193
+ ("repos_removed", repos_removed),
194
+ ("members_added", members_added),
195
+ ("members_removed", members_removed),
196
+ ):
197
+ if values:
198
+ ret["changes"][field] = values
199
+
200
+ if not ret["changes"]:
201
+ return ret
202
+
203
+ verb = "created" if target is None else "updated"
204
+ if __opts__["test"]:
205
+ ret["result"] = None
206
+ ret["comment"] = f"Team {name} would have been {verb}"
207
+ return ret
208
+
209
+ applied = {}
210
+ try:
211
+ if target is None:
212
+ __salt__["github_org.add_team"](
213
+ name, repo_names=repo_names, profile=profile, **settings
214
+ )
215
+ applied["created"] = name
216
+ if repos_added: # Attached during team creation
217
+ applied["repos_added"] = repos_added
218
+ else:
219
+ if "new" in ret["changes"]:
220
+ __salt__["github_org.edit_team"](name, profile=profile, **ret["changes"]["new"])
221
+ applied["old"] = ret["changes"]["old"]
222
+ applied["new"] = ret["changes"]["new"]
223
+ for repo_name in repos_added:
224
+ __salt__["github_org.add_team_repo"](repo_name, name, profile=profile)
225
+ applied.setdefault("repos_added", []).append(repo_name)
226
+ for repo_name in repos_removed:
227
+ __salt__["github_org.remove_team_repo"](repo_name, name, profile=profile)
228
+ applied.setdefault("repos_removed", []).append(repo_name)
229
+ for member in members_added:
230
+ __salt__["github_org.add_team_member"](member, name, profile=profile)
231
+ applied.setdefault("members_added", []).append(member)
232
+ for member in members_removed:
233
+ __salt__["github_org.remove_team_member"](member, name, profile=profile)
234
+ applied.setdefault("members_removed", []).append(member)
235
+ if members_added or members_removed: # Refresh team cache
236
+ __salt__["github_org.list_team_members"](name, profile=profile, ignore_cache=True)
237
+ except CommandExecutionError as err:
238
+ raise StateError(str(err), changes=applied) from err
239
+ ret["comment"] = f"Team {name} has been {verb}"
240
+ return ret
241
+
242
+
243
+ @catch_errors
244
+ def team_absent(name, profile="github"):
245
+ """
246
+ Ensure a named team does not exist.
247
+
248
+ name
249
+ Name of the team in the organization.
250
+
251
+ profile
252
+ Profile configuration name to use. Defaults to ``github``.
253
+ """
254
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
255
+
256
+ try:
257
+ __salt__["github_org.get_team"](name, profile=profile)
258
+ except GitHubNotFoundError:
259
+ ret["comment"] = f"Team {name} does not exist"
260
+ return ret
261
+
262
+ ret["changes"]["deleted"] = name
263
+ if __opts__["test"]:
264
+ ret["result"] = None
265
+ ret["comment"] = f"Team {name} would have been deleted"
266
+ return ret
267
+
268
+ __salt__["github_org.remove_team"](name, profile=profile)
269
+ ret["comment"] = f"Team {name} has been deleted"
270
+ return ret
271
+
272
+
273
+ @catch_errors
274
+ def ruleset_present(name, ruleset_params=None, profile="github"):
275
+ """
276
+ Ensure an organization ruleset exists as specified.
277
+ Only the parameters included in ``ruleset_params`` are compared,
278
+ server-populated fields are ignored.
279
+
280
+ Example:
281
+
282
+ .. code-block:: yaml
283
+
284
+ Ensure ruleset test is present in the org:
285
+ github_org.ruleset_present:
286
+ - name: test
287
+ - ruleset_params:
288
+ target: branch
289
+ enforcement: active
290
+
291
+ name
292
+ Ruleset name to manage.
293
+
294
+ ruleset_params
295
+ Ruleset definition. Must contain at least ``enforcement``
296
+ when the ruleset needs to be created. ``name`` is pulled from
297
+ this state's ``name``.
298
+
299
+ profile
300
+ Profile configuration name to use. Defaults to ``github``.
301
+ """
302
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
303
+
304
+ ruleset_params = dict(ruleset_params or {})
305
+ ruleset_params["name"] = name
306
+
307
+ rulesets = __salt__["github_org.list_rulesets"](profile=profile)
308
+ ruleset = next((rls for rls in rulesets or [] if rls["name"] == name), None)
309
+
310
+ if ruleset is None:
311
+ ret["changes"]["created"] = name
312
+ if __opts__["test"]:
313
+ ret["result"] = None
314
+ ret["comment"] = f"Ruleset {name} would have been created"
315
+ return ret
316
+ __salt__["github_org.add_ruleset"](ruleset_params, profile=profile)
317
+ ret["comment"] = f"Ruleset {name} has been created"
318
+ return ret
319
+
320
+ ruleset_info = __salt__["github_org.get_ruleset"](ruleset["id"], profile=profile)
321
+
322
+ # Only diff the parameters that are being managed. The server
323
+ # response carries additional server-populated fields (id,
324
+ # timestamps, source, ...) that must not register as drift.
325
+ diff = {key: val for key, val in ruleset_params.items() if ruleset_info.get(key) != val}
326
+ if not diff:
327
+ ret["comment"] = f"Ruleset {name} is already configured as specified"
328
+ return ret
329
+
330
+ ret["changes"]["old"] = {key: ruleset_info.get(key) for key in diff}
331
+ ret["changes"]["new"] = diff
332
+ if __opts__["test"]:
333
+ ret["result"] = None
334
+ ret["comment"] = f"Ruleset {name} would have been updated"
335
+ return ret
336
+
337
+ __salt__["github_org.update_ruleset"](ruleset["id"], ruleset_params, profile=profile)
338
+ ret["comment"] = f"Ruleset {name} has been updated"
339
+ return ret
340
+
341
+
342
+ @catch_errors
343
+ def ruleset_absent(name, profile="github"):
344
+ """
345
+ Ensure a named organization ruleset does not exist.
346
+
347
+ name
348
+ Ruleset name to manage.
349
+
350
+ profile
351
+ Profile configuration name to use. Defaults to ``github``.
352
+ """
353
+ ret = {"name": name, "changes": {}, "result": True, "comment": ""}
354
+
355
+ rulesets = __salt__["github_org.list_rulesets"](profile=profile)
356
+ ruleset = next((rls for rls in rulesets or [] if rls["name"] == name), None)
357
+ if ruleset is None:
358
+ ret["comment"] = f"Ruleset {name} does not exist"
359
+ return ret
360
+
361
+ ret["changes"]["deleted"] = name
362
+ if __opts__["test"]:
363
+ ret["result"] = None
364
+ ret["comment"] = f"Ruleset {name} would have been deleted"
365
+ return ret
366
+
367
+ __salt__["github_org.delete_ruleset"](ruleset["id"], profile=profile)
368
+ ret["comment"] = f"Ruleset {name} has been deleted"
369
+ return ret