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.
- saltext/github/__init__.py +31 -0
- saltext/github/modules/__init__.py +0 -0
- saltext/github/modules/github.py +1873 -0
- saltext/github/modules/github_org.py +667 -0
- saltext/github/modules/github_repo.py +1658 -0
- saltext/github/modules/github_user.py +75 -0
- saltext/github/resources/__init__.py +0 -0
- saltext/github/resources/github/__init__.py +251 -0
- saltext/github/resources/github/modules/__init__.py +0 -0
- saltext/github/resources/github/modules/actions.py +175 -0
- saltext/github/resources/github/modules/config.py +69 -0
- saltext/github/resources/github/modules/deploy_key.py +122 -0
- saltext/github/resources/github/modules/environment.py +241 -0
- saltext/github/resources/github/modules/grains.py +18 -0
- saltext/github/resources/github/modules/issue.py +157 -0
- saltext/github/resources/github/modules/label.py +161 -0
- saltext/github/resources/github/modules/milestone.py +103 -0
- saltext/github/resources/github/modules/pages.py +149 -0
- saltext/github/resources/github/modules/pull.py +90 -0
- saltext/github/resources/github/modules/repo.py +155 -0
- saltext/github/resources/github/modules/ruleset.py +144 -0
- saltext/github/resources/github/modules/saltutil.py +24 -0
- saltext/github/resources/github/modules/state.py +116 -0
- saltext/github/resources/github/modules/test.py +29 -0
- saltext/github/resources/github/states/__init__.py +0 -0
- saltext/github/resources/github/states/actions.py +117 -0
- saltext/github/resources/github/states/deploy_key.py +130 -0
- saltext/github/resources/github/states/environment.py +99 -0
- saltext/github/resources/github/states/label.py +71 -0
- saltext/github/resources/github/states/pages.py +92 -0
- saltext/github/resources/github/states/repo.py +108 -0
- saltext/github/resources/github/states/ruleset.py +81 -0
- saltext/github/states/__init__.py +0 -0
- saltext/github/states/github.py +966 -0
- saltext/github/states/github_org.py +369 -0
- saltext/github/states/github_repo.py +722 -0
- saltext/github/utils/__init__.py +72 -0
- saltext/github/utils/gh/__init__.py +14 -0
- saltext/github/utils/gh/actions.py +195 -0
- saltext/github/utils/gh/cache.py +126 -0
- saltext/github/utils/gh/client.py +534 -0
- saltext/github/utils/gh/deploy_keys.py +121 -0
- saltext/github/utils/gh/environments.py +271 -0
- saltext/github/utils/gh/exceptions.py +28 -0
- saltext/github/utils/gh/grains_files.py +366 -0
- saltext/github/utils/gh/issues.py +193 -0
- saltext/github/utils/gh/labels.py +142 -0
- saltext/github/utils/gh/members.py +157 -0
- saltext/github/utils/gh/milestones.py +122 -0
- saltext/github/utils/gh/pages.py +128 -0
- saltext/github/utils/gh/profile.py +458 -0
- saltext/github/utils/gh/pulls.py +99 -0
- saltext/github/utils/gh/repos.py +465 -0
- saltext/github/utils/gh/resource.py +35 -0
- saltext/github/utils/gh/rulesets.py +191 -0
- saltext/github/utils/gh/statecores/__init__.py +70 -0
- saltext/github/utils/gh/statecores/actions.py +180 -0
- saltext/github/utils/gh/statecores/deploy_keys.py +241 -0
- saltext/github/utils/gh/statecores/environments.py +212 -0
- saltext/github/utils/gh/statecores/labels.py +116 -0
- saltext/github/utils/gh/statecores/pages.py +177 -0
- saltext/github/utils/gh/statecores/repo.py +249 -0
- saltext/github/utils/gh/statecores/rulesets.py +115 -0
- saltext/github/utils/gh/teams.py +375 -0
- saltext/github/utils/gh/users.py +87 -0
- saltext/github/version.py +1 -0
- saltext_github-1.1.0.dist-info/METADATA +151 -0
- saltext_github-1.1.0.dist-info/RECORD +73 -0
- saltext_github-1.1.0.dist-info/WHEEL +5 -0
- saltext_github-1.1.0.dist-info/entry_points.txt +2 -0
- saltext_github-1.1.0.dist-info/licenses/LICENSE +201 -0
- saltext_github-1.1.0.dist-info/licenses/NOTICE +10 -0
- 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
|