qontract-reconcile 0.10.2.dev134__py3-none-any.whl → 0.10.2.dev136__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.
- {qontract_reconcile-0.10.2.dev134.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.2.dev134.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/RECORD +37 -31
- reconcile/aws_ami_share.py +4 -2
- reconcile/aws_ecr_image_pull_secrets.py +15 -7
- reconcile/aws_garbage_collector.py +1 -1
- reconcile/aws_iam_keys.py +24 -17
- reconcile/aws_iam_password_reset.py +4 -2
- reconcile/aws_support_cases_sos.py +19 -6
- reconcile/closedbox_endpoint_monitoring_base.py +4 -3
- reconcile/cna/client.py +3 -3
- reconcile/cna/integration.py +4 -5
- reconcile/cna/state.py +3 -3
- reconcile/email_sender.py +65 -56
- reconcile/gcr_mirror.py +11 -9
- reconcile/github_org.py +57 -52
- reconcile/github_owners.py +8 -5
- reconcile/github_repo_invites.py +1 -1
- reconcile/github_repo_permissions_validator.py +3 -3
- reconcile/github_users.py +16 -12
- reconcile/github_validator.py +1 -1
- reconcile/gitlab_fork_compliance.py +9 -8
- reconcile/gitlab_labeler.py +1 -1
- reconcile/gitlab_mr_sqs_consumer.py +6 -3
- reconcile/gitlab_owners.py +29 -12
- reconcile/gql_definitions/email_sender/__init__.py +0 -0
- reconcile/gql_definitions/email_sender/apps.py +64 -0
- reconcile/gql_definitions/email_sender/emails.py +133 -0
- reconcile/gql_definitions/email_sender/users.py +62 -0
- reconcile/gql_definitions/fragments/email_service.py +32 -0
- reconcile/gql_definitions/fragments/email_user.py +28 -0
- reconcile/queries.py +0 -44
- reconcile/utils/gitlab_api.py +24 -64
- reconcile/utils/instrumented_wrappers.py +32 -4
- reconcile/utils/jinja2/utils.py +4 -2
- reconcile/utils/smtp_client.py +1 -1
- {qontract_reconcile-0.10.2.dev134.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev134.dist-info → qontract_reconcile-0.10.2.dev136.dist-info}/entry_points.txt +0 -0
reconcile/email_sender.py
CHANGED
@@ -1,13 +1,21 @@
|
|
1
1
|
import logging
|
2
2
|
import sys
|
3
|
+
from collections.abc import Callable
|
3
4
|
|
4
|
-
from reconcile import
|
5
|
-
|
6
|
-
|
5
|
+
from reconcile import typed_queries
|
6
|
+
from reconcile.gql_definitions.email_sender.apps import query as apps_query
|
7
|
+
from reconcile.gql_definitions.email_sender.emails import (
|
8
|
+
AppInterfaceEmailAudienceV1,
|
9
|
+
AppInterfaceEmailV1,
|
7
10
|
)
|
11
|
+
from reconcile.gql_definitions.email_sender.emails import query as emails_query
|
12
|
+
from reconcile.gql_definitions.email_sender.users import query as users_query
|
13
|
+
from reconcile.gql_definitions.fragments.email_service import EmailServiceOwners
|
14
|
+
from reconcile.gql_definitions.fragments.email_user import EmailUser
|
8
15
|
from reconcile.typed_queries.app_interface_vault_settings import (
|
9
16
|
get_app_interface_vault_settings,
|
10
17
|
)
|
18
|
+
from reconcile.utils import gql
|
11
19
|
from reconcile.utils.defer import defer
|
12
20
|
from reconcile.utils.secret_reader import create_secret_reader
|
13
21
|
from reconcile.utils.smtp_client import (
|
@@ -20,11 +28,17 @@ from reconcile.utils.state import init_state
|
|
20
28
|
QONTRACT_INTEGRATION = "email-sender"
|
21
29
|
|
22
30
|
|
23
|
-
def collect_to(
|
31
|
+
def collect_to(
|
32
|
+
to: AppInterfaceEmailAudienceV1,
|
33
|
+
all_users: list[EmailUser],
|
34
|
+
all_services: list[EmailServiceOwners],
|
35
|
+
) -> set[str]:
|
24
36
|
"""Collect audience to send email to from to object
|
25
37
|
|
26
38
|
Arguments:
|
27
|
-
to
|
39
|
+
to -- AppInterfaceEmailAudience_v1 object
|
40
|
+
all_users -- List of all app-interface users
|
41
|
+
all_services -- List of all app-interface apps/services with owners
|
28
42
|
|
29
43
|
Raises:
|
30
44
|
AttributeError: Unknown alias
|
@@ -32,63 +46,58 @@ def collect_to(to):
|
|
32
46
|
Returns:
|
33
47
|
set -- Audience to send email to
|
34
48
|
"""
|
35
|
-
audience = set()
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
to
|
43
|
-
|
44
|
-
services = queries.get_apps()
|
45
|
-
to["services"] = services
|
46
|
-
else:
|
49
|
+
audience: set[str] = set()
|
50
|
+
|
51
|
+
for alias in to.aliases or []:
|
52
|
+
match alias:
|
53
|
+
case "all-users":
|
54
|
+
to.users = all_users
|
55
|
+
case "all-service-owners":
|
56
|
+
to.services = all_services
|
57
|
+
case _:
|
47
58
|
raise AttributeError(f"unknown alias: {alias}")
|
48
59
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
if not service_owners:
|
54
|
-
continue
|
60
|
+
for service in to.services or []:
|
61
|
+
audience.update(
|
62
|
+
service_owner.email for service_owner in service.service_owners or []
|
63
|
+
)
|
55
64
|
|
56
|
-
|
65
|
+
for account in to.aws_accounts or []:
|
66
|
+
audience.update(
|
67
|
+
account_owner.email for account_owner in account.account_owners or []
|
68
|
+
)
|
57
69
|
|
58
|
-
|
70
|
+
for role in to.roles or []:
|
71
|
+
audience.update(user.org_username for user in role.users or [])
|
59
72
|
|
60
|
-
|
61
|
-
|
62
|
-
for account in aws_accounts:
|
63
|
-
account_owners = account.get("accountOwners")
|
64
|
-
if not account_owners:
|
65
|
-
continue
|
73
|
+
# SmtpClient supports sending to org_username and email addresses
|
74
|
+
audience.update(user.org_username for user in to.users or [])
|
66
75
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
if roles:
|
71
|
-
for role in roles:
|
72
|
-
users = role.get("users")
|
73
|
-
if not users:
|
74
|
-
continue
|
75
|
-
|
76
|
-
audience.update(user["org_username"] for user in users)
|
76
|
+
if to.clusters or to.namespaces:
|
77
|
+
raise NotImplementedError("clusters and namespaces are not implemented yet")
|
78
|
+
return audience
|
77
79
|
|
78
|
-
users = to.get("users")
|
79
|
-
if users:
|
80
|
-
audience.update(user["org_username"] for user in users)
|
81
80
|
|
82
|
-
|
81
|
+
def get_emails(query_func: Callable) -> list[AppInterfaceEmailV1]:
|
82
|
+
return emails_query(query_func).emails or []
|
83
83
|
|
84
84
|
|
85
85
|
@defer
|
86
|
-
def run(dry_run, defer=None):
|
86
|
+
def run(dry_run: bool, defer: Callable | None = None) -> None:
|
87
|
+
gql_api = gql.get_api()
|
87
88
|
vault_settings = get_app_interface_vault_settings()
|
88
89
|
secret_reader = create_secret_reader(use_vault=vault_settings.vault)
|
89
90
|
state = init_state(integration=QONTRACT_INTEGRATION, secret_reader=secret_reader)
|
90
|
-
defer
|
91
|
-
|
91
|
+
if defer:
|
92
|
+
defer(state.cleanup)
|
93
|
+
|
94
|
+
emails = get_emails(gql_api.query)
|
95
|
+
if not emails:
|
96
|
+
logging.info("no emails to send")
|
97
|
+
sys.exit(0)
|
98
|
+
|
99
|
+
all_users = users_query(gql_api.query).users or []
|
100
|
+
all_services = apps_query(gql_api.query).apps or []
|
92
101
|
smtp_settings = typed_queries.smtp.settings()
|
93
102
|
smtp_client = SmtpClient(
|
94
103
|
server=get_smtp_server_connection(
|
@@ -99,18 +108,18 @@ def run(dry_run, defer=None):
|
|
99
108
|
timeout=smtp_settings.timeout or DEFAULT_SMTP_TIMEOUT,
|
100
109
|
)
|
101
110
|
# validate no 2 emails have the same name
|
102
|
-
email_names = {e
|
111
|
+
email_names = {e.name for e in emails}
|
103
112
|
if len(emails) != len(email_names):
|
104
113
|
logging.error("email names must be unique.")
|
105
114
|
sys.exit(1)
|
106
115
|
|
107
|
-
emails_to_send = [e for e in emails if not state.exists(e
|
116
|
+
emails_to_send = [e for e in emails if not state.exists(e.name)]
|
108
117
|
for email in emails_to_send:
|
109
|
-
logging.info(["send_email", email
|
118
|
+
logging.info(["send_email", email.name, email.subject])
|
110
119
|
|
111
120
|
if not dry_run:
|
112
|
-
names = collect_to(
|
113
|
-
|
114
|
-
|
115
|
-
smtp_client.send_mail(names, subject, body)
|
116
|
-
state.add(email
|
121
|
+
names = collect_to(
|
122
|
+
email.q_to, all_users=all_users, all_services=all_services
|
123
|
+
)
|
124
|
+
smtp_client.send_mail(names, email.subject, email.body)
|
125
|
+
state.add(email.name)
|
reconcile/gcr_mirror.py
CHANGED
@@ -66,7 +66,7 @@ class QuayMirror:
|
|
66
66
|
}
|
67
67
|
"""
|
68
68
|
|
69
|
-
def __init__(self, dry_run=False):
|
69
|
+
def __init__(self, dry_run: bool = False) -> None:
|
70
70
|
self.dry_run = dry_run
|
71
71
|
self.gqlapi = gql.get_api()
|
72
72
|
settings = queries.get_app_interface_settings()
|
@@ -81,7 +81,7 @@ class QuayMirror:
|
|
81
81
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
|
82
82
|
self.session.close()
|
83
83
|
|
84
|
-
def run(self):
|
84
|
+
def run(self) -> None:
|
85
85
|
sync_tasks = self.process_sync_tasks()
|
86
86
|
for org, data in sync_tasks.items():
|
87
87
|
for item in data:
|
@@ -95,7 +95,7 @@ class QuayMirror:
|
|
95
95
|
except SkopeoCmdError as details:
|
96
96
|
_LOG.error("[%s]", details)
|
97
97
|
|
98
|
-
def process_repos_query(self):
|
98
|
+
def process_repos_query(self) -> dict[str, list[dict[str, Any]]]:
|
99
99
|
result = self.gqlapi.query(self.GCR_REPOS_QUERY)
|
100
100
|
|
101
101
|
summary = defaultdict(list)
|
@@ -122,7 +122,9 @@ class QuayMirror:
|
|
122
122
|
return summary
|
123
123
|
|
124
124
|
@staticmethod
|
125
|
-
def sync_tag(
|
125
|
+
def sync_tag(
|
126
|
+
tags: list[str] | None, tags_exclude: list[str] | None, candidate: str
|
127
|
+
) -> bool:
|
126
128
|
if tags is not None:
|
127
129
|
# When tags is defined, we don't look at tags_exclude
|
128
130
|
return any(re.match(tag, candidate) for tag in tags)
|
@@ -137,7 +139,7 @@ class QuayMirror:
|
|
137
139
|
# tag must be synced
|
138
140
|
return True
|
139
141
|
|
140
|
-
def process_sync_tasks(self):
|
142
|
+
def process_sync_tasks(self) -> dict[str, list[dict[str, Any]]]:
|
141
143
|
eight_hours = 28800 # 60 * 60 * 8
|
142
144
|
is_deep_sync = self._is_deep_sync(interval=eight_hours)
|
143
145
|
|
@@ -233,7 +235,7 @@ class QuayMirror:
|
|
233
235
|
|
234
236
|
return sync_tasks
|
235
237
|
|
236
|
-
def _is_deep_sync(self, interval):
|
238
|
+
def _is_deep_sync(self, interval: int) -> bool:
|
237
239
|
control_file_name = "qontract-reconcile-gcr-mirror.timestamp"
|
238
240
|
control_file_path = os.path.join(tempfile.gettempdir(), control_file_name)
|
239
241
|
try:
|
@@ -251,11 +253,11 @@ class QuayMirror:
|
|
251
253
|
return False
|
252
254
|
|
253
255
|
@staticmethod
|
254
|
-
def _record_timestamp(path):
|
256
|
+
def _record_timestamp(path: str) -> None:
|
255
257
|
with open(path, "w", encoding="locale") as file_object:
|
256
258
|
file_object.write(str(time.time()))
|
257
259
|
|
258
|
-
def _get_push_creds(self):
|
260
|
+
def _get_push_creds(self) -> dict[str, str]:
|
259
261
|
result = self.gqlapi.query(self.GCR_PROJECT_CATALOG_QUERY)
|
260
262
|
|
261
263
|
creds = {}
|
@@ -271,6 +273,6 @@ class QuayMirror:
|
|
271
273
|
return creds
|
272
274
|
|
273
275
|
|
274
|
-
def run(dry_run):
|
276
|
+
def run(dry_run: bool) -> None:
|
275
277
|
with QuayMirror(dry_run) as gcr_mirror:
|
276
278
|
gcr_mirror.run()
|
reconcile/github_org.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
|
+
from collections.abc import Callable, Iterable, KeysView
|
3
4
|
from typing import Any
|
4
5
|
|
5
6
|
from github import Github
|
6
7
|
from github.GithubObject import NotSet # type: ignore
|
8
|
+
from github.Organization import Organization
|
9
|
+
from github.Team import Team
|
7
10
|
from sretoolbox.utils import retry
|
8
11
|
|
9
12
|
from reconcile import (
|
@@ -95,15 +98,15 @@ CLUSTERS_QUERY = """
|
|
95
98
|
QONTRACT_INTEGRATION = "github"
|
96
99
|
|
97
100
|
|
98
|
-
def get_orgs():
|
101
|
+
def get_orgs() -> list[dict[str, Any]]:
|
99
102
|
gqlapi = gql.get_api()
|
100
103
|
return gqlapi.query(ORGS_QUERY)["orgs"]
|
101
104
|
|
102
105
|
|
103
|
-
def get_config(default=False):
|
106
|
+
def get_config(default: bool = False) -> dict[str, Any]:
|
104
107
|
orgs = get_orgs()
|
105
108
|
secret_reader = SecretReader(queries.get_secret_reader_settings())
|
106
|
-
config = {"github": {}}
|
109
|
+
config: dict[str, Any] = {"github": {}}
|
107
110
|
found_defaults = []
|
108
111
|
for org in orgs:
|
109
112
|
org_name = org["name"]
|
@@ -126,24 +129,52 @@ def get_config(default=False):
|
|
126
129
|
return config
|
127
130
|
|
128
131
|
|
129
|
-
def get_default_config():
|
132
|
+
def get_default_config() -> dict[str, Any]:
|
130
133
|
github_config = get_config(default=True)
|
131
134
|
return next(iter(github_config["github"].values()))
|
132
135
|
|
133
136
|
|
134
137
|
@retry()
|
135
|
-
def get_org_and_teams(
|
138
|
+
def get_org_and_teams(
|
139
|
+
github: Github, org_name: str
|
140
|
+
) -> tuple[Organization, Iterable[Team]]:
|
136
141
|
org = github.get_organization(org_name)
|
137
142
|
teams = org.get_teams()
|
138
143
|
return org, teams
|
139
144
|
|
140
145
|
|
141
146
|
@retry()
|
142
|
-
def get_members(unit):
|
147
|
+
def get_members(unit: Organization) -> list[str]:
|
143
148
|
return [member.login for member in unit.get_members()]
|
144
149
|
|
145
150
|
|
146
|
-
|
151
|
+
class GHApiStore:
|
152
|
+
_orgs: dict[str, Any] = {}
|
153
|
+
|
154
|
+
def __init__(self, config: dict) -> None:
|
155
|
+
for org_name, org_config in config["github"].items():
|
156
|
+
token = org_config["token"]
|
157
|
+
managed_teams = org_config.get("managed_teams", None)
|
158
|
+
self._orgs[org_name] = (
|
159
|
+
Github(token, base_url=GH_BASE_URL),
|
160
|
+
RawGithubApi(token),
|
161
|
+
managed_teams,
|
162
|
+
)
|
163
|
+
|
164
|
+
def orgs(self) -> KeysView[str]:
|
165
|
+
return self._orgs.keys()
|
166
|
+
|
167
|
+
def github(self, org_name: str) -> Github:
|
168
|
+
return self._orgs[org_name][0]
|
169
|
+
|
170
|
+
def raw_github_api(self, org_name: str) -> RawGithubApi:
|
171
|
+
return self._orgs[org_name][1]
|
172
|
+
|
173
|
+
def managed_teams(self, org_name: str) -> list[str] | None:
|
174
|
+
return self._orgs[org_name][2]
|
175
|
+
|
176
|
+
|
177
|
+
def fetch_current_state(gh_api_store: GHApiStore) -> AggregatedList:
|
147
178
|
state = AggregatedList()
|
148
179
|
|
149
180
|
for org_name in gh_api_store.orgs():
|
@@ -164,7 +195,7 @@ def fetch_current_state(gh_api_store):
|
|
164
195
|
|
165
196
|
all_team_members = []
|
166
197
|
for team in teams:
|
167
|
-
if not is_managed and team.name not in managed_teams:
|
198
|
+
if not is_managed and team.name not in (managed_teams or []):
|
168
199
|
continue
|
169
200
|
|
170
201
|
members = get_members(team)
|
@@ -190,11 +221,11 @@ def fetch_current_state(gh_api_store):
|
|
190
221
|
return state
|
191
222
|
|
192
223
|
|
193
|
-
def fetch_desired_state(infer_clusters=True):
|
224
|
+
def fetch_desired_state(infer_clusters: bool = True) -> AggregatedList:
|
194
225
|
gqlapi = gql.get_api()
|
195
226
|
state = AggregatedList()
|
196
227
|
|
197
|
-
roles = expiration.filter(gqlapi.query(ROLES_QUERY)["roles"])
|
228
|
+
roles: list[dict[str, Any]] = expiration.filter(gqlapi.query(ROLES_QUERY)["roles"])
|
198
229
|
for role in roles:
|
199
230
|
permissions = list(
|
200
231
|
filter(
|
@@ -264,41 +295,15 @@ def fetch_desired_state(infer_clusters=True):
|
|
264
295
|
return state
|
265
296
|
|
266
297
|
|
267
|
-
class GHApiStore:
|
268
|
-
_orgs: dict[str, Any] = {}
|
269
|
-
|
270
|
-
def __init__(self, config):
|
271
|
-
for org_name, org_config in config["github"].items():
|
272
|
-
token = org_config["token"]
|
273
|
-
managed_teams = org_config.get("managed_teams", None)
|
274
|
-
self._orgs[org_name] = (
|
275
|
-
Github(token, base_url=GH_BASE_URL),
|
276
|
-
RawGithubApi(token),
|
277
|
-
managed_teams,
|
278
|
-
)
|
279
|
-
|
280
|
-
def orgs(self):
|
281
|
-
return self._orgs.keys()
|
282
|
-
|
283
|
-
def github(self, org_name):
|
284
|
-
return self._orgs[org_name][0]
|
285
|
-
|
286
|
-
def raw_github_api(self, org_name):
|
287
|
-
return self._orgs[org_name][1]
|
288
|
-
|
289
|
-
def managed_teams(self, org_name):
|
290
|
-
return self._orgs[org_name][2]
|
291
|
-
|
292
|
-
|
293
298
|
class RunnerAction:
|
294
|
-
def __init__(self, dry_run, gh_api_store):
|
299
|
+
def __init__(self, dry_run: bool, gh_api_store: GHApiStore) -> None:
|
295
300
|
self.dry_run = dry_run
|
296
301
|
self.gh_api_store = gh_api_store
|
297
302
|
|
298
|
-
def add_to_team(self):
|
303
|
+
def add_to_team(self) -> Callable:
|
299
304
|
label = "add_to_team"
|
300
305
|
|
301
|
-
def action(params, items):
|
306
|
+
def action(params: dict, items: dict) -> None:
|
302
307
|
org = params["org"]
|
303
308
|
team = params["team"]
|
304
309
|
|
@@ -318,10 +323,10 @@ class RunnerAction:
|
|
318
323
|
|
319
324
|
return action
|
320
325
|
|
321
|
-
def del_from_team(self):
|
326
|
+
def del_from_team(self) -> Callable:
|
322
327
|
label = "del_from_team"
|
323
328
|
|
324
|
-
def action(params, items):
|
329
|
+
def action(params: dict, items: dict) -> None:
|
325
330
|
org = params["org"]
|
326
331
|
team = params["team"]
|
327
332
|
|
@@ -346,10 +351,10 @@ class RunnerAction:
|
|
346
351
|
|
347
352
|
return action
|
348
353
|
|
349
|
-
def create_team(self):
|
354
|
+
def create_team(self) -> Callable:
|
350
355
|
label = "create_team"
|
351
356
|
|
352
|
-
def action(params, items):
|
357
|
+
def action(params: dict, items: dict) -> None:
|
353
358
|
org = params["org"]
|
354
359
|
team = params["team"]
|
355
360
|
|
@@ -367,10 +372,10 @@ class RunnerAction:
|
|
367
372
|
|
368
373
|
return action
|
369
374
|
|
370
|
-
def add_to_org(self):
|
375
|
+
def add_to_org(self) -> Callable:
|
371
376
|
label = "add_to_org"
|
372
377
|
|
373
|
-
def action(params, items):
|
378
|
+
def action(params: dict, items: dict) -> None:
|
374
379
|
org = params["org"]
|
375
380
|
|
376
381
|
if self.dry_run:
|
@@ -387,10 +392,10 @@ class RunnerAction:
|
|
387
392
|
|
388
393
|
return action
|
389
394
|
|
390
|
-
def del_from_org(self):
|
395
|
+
def del_from_org(self) -> Callable:
|
391
396
|
label = "del_from_org"
|
392
397
|
|
393
|
-
def action(params, items):
|
398
|
+
def action(params: dict, items: dict) -> None:
|
394
399
|
org = params["org"]
|
395
400
|
|
396
401
|
if self.dry_run:
|
@@ -410,18 +415,18 @@ class RunnerAction:
|
|
410
415
|
return action
|
411
416
|
|
412
417
|
@staticmethod
|
413
|
-
def raise_exception(msg):
|
414
|
-
def raiseException(params, items):
|
418
|
+
def raise_exception(msg: str) -> Callable:
|
419
|
+
def raiseException(params: dict, items: dict) -> None:
|
415
420
|
raise Exception(msg)
|
416
421
|
|
417
422
|
return raiseException
|
418
423
|
|
419
424
|
|
420
|
-
def service_is(service):
|
425
|
+
def service_is(service: str) -> Callable:
|
421
426
|
return lambda params: params.get("service") == service
|
422
427
|
|
423
428
|
|
424
|
-
def run(dry_run):
|
429
|
+
def run(dry_run: bool) -> None:
|
425
430
|
config = get_config()
|
426
431
|
gh_api_store = GHApiStore(config)
|
427
432
|
|
@@ -507,7 +512,7 @@ def run(dry_run):
|
|
507
512
|
runner.run()
|
508
513
|
|
509
514
|
|
510
|
-
def early_exit_desired_state(*args, **kwargs) -> dict[str, Any]:
|
515
|
+
def early_exit_desired_state(*args: Any, **kwargs: Any) -> dict[str, Any]:
|
511
516
|
return {
|
512
517
|
"github_orgs": get_orgs(),
|
513
518
|
"github_org_members": fetch_desired_state().dump(),
|
reconcile/github_owners.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
|
4
|
+
import github
|
4
5
|
from github import Github
|
5
6
|
from sretoolbox.utils import retry
|
6
7
|
|
@@ -40,10 +41,10 @@ ROLES_QUERY = """
|
|
40
41
|
QONTRACT_INTEGRATION = "github-owners"
|
41
42
|
|
42
43
|
|
43
|
-
def fetch_desired_state():
|
44
|
-
desired_state = {}
|
44
|
+
def fetch_desired_state() -> dict[str, list[str]]:
|
45
|
+
desired_state: dict[str, list[str]] = {}
|
45
46
|
gqlapi = gql.get_api()
|
46
|
-
roles = expiration.filter(gqlapi.query(ROLES_QUERY)["roles"])
|
47
|
+
roles: list[dict] = expiration.filter(gqlapi.query(ROLES_QUERY)["roles"])
|
47
48
|
for role in roles:
|
48
49
|
permissions = [
|
49
50
|
p
|
@@ -67,7 +68,9 @@ def fetch_desired_state():
|
|
67
68
|
|
68
69
|
|
69
70
|
@retry()
|
70
|
-
def get_current_github_usernames(
|
71
|
+
def get_current_github_usernames(
|
72
|
+
github_org_name: str, github: Github, raw_github: RawGithubApi
|
73
|
+
) -> tuple[github.Organization.Organization, list[str]]:
|
71
74
|
gh_org = github.get_organization(github_org_name)
|
72
75
|
gh_org_members = gh_org.get_members(role="admin")
|
73
76
|
current_github_usernames = [m.login for m in gh_org_members]
|
@@ -77,7 +80,7 @@ def get_current_github_usernames(github_org_name, github, raw_github):
|
|
77
80
|
return gh_org, current_github_usernames
|
78
81
|
|
79
82
|
|
80
|
-
def run(dry_run):
|
83
|
+
def run(dry_run: bool) -> None:
|
81
84
|
base_url = os.environ.get("GITHUB_API", "https://api.github.com")
|
82
85
|
config = get_config()
|
83
86
|
desired_state = fetch_desired_state()
|
reconcile/github_repo_invites.py
CHANGED
@@ -95,7 +95,7 @@ def get_settings() -> Mapping[str, Any]:
|
|
95
95
|
raise ValueError("no app-interface-settings found")
|
96
96
|
|
97
97
|
|
98
|
-
def run(dry_run):
|
98
|
+
def run(dry_run: bool) -> set[str]:
|
99
99
|
gqlapi = gql.get_api()
|
100
100
|
settings = get_settings()
|
101
101
|
secret_reader = SecretReader(settings=settings)
|
@@ -16,7 +16,7 @@ QONTRACT_INTEGRATION = "github-repo-permissions-validator"
|
|
16
16
|
QONTRACT_INTEGRATION_VERSION = make_semver(0, 1, 0)
|
17
17
|
|
18
18
|
|
19
|
-
def get_jobs(jjb: JJB, instance_name: str):
|
19
|
+
def get_jobs(jjb: JJB, instance_name: str) -> list[dict] | None:
|
20
20
|
pr_check_jobs = jjb.get_all_jobs(
|
21
21
|
job_types=["gh-pr-check"], instance_name=instance_name
|
22
22
|
).get(instance_name)
|
@@ -24,13 +24,13 @@ def get_jobs(jjb: JJB, instance_name: str):
|
|
24
24
|
return pr_check_jobs
|
25
25
|
|
26
26
|
|
27
|
-
def init_github():
|
27
|
+
def init_github() -> Github:
|
28
28
|
base_url = os.environ.get("GITHUB_API", "https://api.github.com")
|
29
29
|
token = get_default_config()["token"]
|
30
30
|
return Github(token, base_url=base_url)
|
31
31
|
|
32
32
|
|
33
|
-
def run(dry_run, instance_name):
|
33
|
+
def run(dry_run: bool, instance_name: str) -> None:
|
34
34
|
secret_reader = SecretReader(queries.get_secret_reader_settings())
|
35
35
|
jjb: JJB = init_jjb(secret_reader)
|
36
36
|
pr_check_jobs = get_jobs(jjb, instance_name)
|
reconcile/github_users.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
import re
|
4
|
+
from collections.abc import Callable
|
4
5
|
|
5
6
|
from github import Github
|
6
7
|
from github.GithubException import GithubException
|
@@ -30,19 +31,21 @@ GH_BASE_URL = os.environ.get("GITHUB_API", "https://api.github.com")
|
|
30
31
|
|
31
32
|
QONTRACT_INTEGRATION = "github-users"
|
32
33
|
|
34
|
+
UserAndCompany = tuple[str, str | None]
|
33
35
|
|
34
|
-
|
36
|
+
|
37
|
+
def init_github() -> Github:
|
35
38
|
token = get_default_config()["token"]
|
36
39
|
return Github(token, base_url=GH_BASE_URL)
|
37
40
|
|
38
41
|
|
39
42
|
@retry(exceptions=(GithubException, ReadTimeout))
|
40
|
-
def get_user_company(user, github):
|
43
|
+
def get_user_company(user: dict, github: Github) -> UserAndCompany:
|
41
44
|
gh_user = github.get_user(login=user["github_username"])
|
42
45
|
return user["org_username"], gh_user.company
|
43
46
|
|
44
47
|
|
45
|
-
def get_users_to_delete(results):
|
48
|
+
def get_users_to_delete(results: list[UserAndCompany]) -> list[dict]:
|
46
49
|
pattern = r"^.*[Rr]ed ?[Hh]at.*$"
|
47
50
|
org_usernames_to_delete = [
|
48
51
|
u for u, c in results if c is None or not re.search(pattern, c)
|
@@ -51,7 +54,7 @@ def get_users_to_delete(results):
|
|
51
54
|
return [u for u in users_and_paths if u["username"] in org_usernames_to_delete]
|
52
55
|
|
53
56
|
|
54
|
-
def send_email_notification(user, smtp_client: SmtpClient):
|
57
|
+
def send_email_notification(user: dict, smtp_client: SmtpClient) -> None:
|
55
58
|
msg_template = """
|
56
59
|
Hello,
|
57
60
|
|
@@ -79,13 +82,13 @@ App-Interface repository: https://gitlab.cee.redhat.com/service/app-interface
|
|
79
82
|
|
80
83
|
@defer
|
81
84
|
def run(
|
82
|
-
dry_run,
|
83
|
-
gitlab_project_id=None,
|
84
|
-
thread_pool_size=10,
|
85
|
-
enable_deletion=False,
|
86
|
-
send_mails=False,
|
87
|
-
defer=None,
|
88
|
-
):
|
85
|
+
dry_run: bool,
|
86
|
+
gitlab_project_id: str | None = None,
|
87
|
+
thread_pool_size: int = 10,
|
88
|
+
enable_deletion: bool = False,
|
89
|
+
send_mails: bool = False,
|
90
|
+
defer: Callable | None = None,
|
91
|
+
) -> None:
|
89
92
|
smtp_settings = typed_queries.smtp.settings()
|
90
93
|
smtp_client = SmtpClient(
|
91
94
|
server=get_smtp_server_connection(
|
@@ -104,7 +107,8 @@ def run(
|
|
104
107
|
|
105
108
|
if not dry_run and enable_deletion:
|
106
109
|
mr_cli = mr_client_gateway.init(gitlab_project_id=gitlab_project_id)
|
107
|
-
defer
|
110
|
+
if defer:
|
111
|
+
defer(mr_cli.cleanup)
|
108
112
|
|
109
113
|
for user in users_to_delete:
|
110
114
|
username = user["username"]
|
reconcile/github_validator.py
CHANGED
@@ -10,7 +10,7 @@ from reconcile.utils.secret_reader import SecretReader
|
|
10
10
|
QONTRACT_INTEGRATION = "github-validator"
|
11
11
|
|
12
12
|
|
13
|
-
def run(dry_run):
|
13
|
+
def run(dry_run: bool) -> None:
|
14
14
|
base_url = os.environ.get("GITHUB_API", "https://api.github.com")
|
15
15
|
orgs = queries.get_github_orgs()
|
16
16
|
settings = queries.get_app_interface_settings()
|