qontract-reconcile 0.10.2.dev456__py3-none-any.whl → 0.10.2.dev473__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.
Potentially problematic release.
This version of qontract-reconcile might be problematic. Click here for more details.
- {qontract_reconcile-0.10.2.dev456.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/METADATA +2 -2
- {qontract_reconcile-0.10.2.dev456.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/RECORD +26 -26
- reconcile/aus/base.py +0 -3
- reconcile/aws_account_manager/integration.py +13 -1
- reconcile/aws_account_manager/utils.py +1 -1
- reconcile/change_owners/README.md +1 -1
- reconcile/change_owners/change_owners.py +9 -9
- reconcile/change_owners/decision.py +1 -1
- reconcile/gql_definitions/aws_account_manager/aws_accounts.py +9 -0
- reconcile/gql_definitions/external_resources/external_resources_namespaces.py +3 -1
- reconcile/gql_definitions/introspection.json +15 -7
- reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +3 -1
- reconcile/quay_base.py +25 -6
- reconcile/quay_membership.py +35 -28
- reconcile/quay_mirror_org.py +6 -4
- reconcile/quay_permissions.py +81 -75
- reconcile/quay_repos.py +35 -37
- reconcile/queries.py +1 -1
- reconcile/templating/validator.py +4 -4
- reconcile/terraform_vpc_resources/merge_request.py +12 -2
- reconcile/terraform_vpc_resources/merge_request_manager.py +43 -19
- reconcile/typed_queries/saas_files.py +1 -1
- reconcile/utils/external_resource_spec.py +2 -0
- reconcile/utils/quay_api.py +74 -87
- {qontract_reconcile-0.10.2.dev456.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.2.dev456.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/entry_points.txt +0 -0
reconcile/utils/quay_api.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
import requests
|
|
4
5
|
|
|
6
|
+
from reconcile.utils.rest_api_base import ApiBase, BearerTokenAuth
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class QuayTeamNotFoundError(Exception):
|
|
7
10
|
pass
|
|
8
11
|
|
|
9
12
|
|
|
10
|
-
class QuayApi:
|
|
13
|
+
class QuayApi(ApiBase):
|
|
11
14
|
LIMIT_FOLLOWS = 15
|
|
12
15
|
|
|
13
16
|
def __init__(
|
|
@@ -17,14 +20,18 @@ class QuayApi:
|
|
|
17
20
|
base_url: str = "quay.io",
|
|
18
21
|
timeout: int = 60,
|
|
19
22
|
) -> None:
|
|
20
|
-
|
|
23
|
+
# Support both hostname (e.g., "quay.io") and full URLs (e.g., "http://localhost:12345")
|
|
24
|
+
if base_url.startswith(("http://", "https://")):
|
|
25
|
+
host = base_url
|
|
26
|
+
else:
|
|
27
|
+
host = f"https://{base_url}"
|
|
28
|
+
super().__init__(
|
|
29
|
+
host=host,
|
|
30
|
+
auth=BearerTokenAuth(token),
|
|
31
|
+
read_timeout=timeout,
|
|
32
|
+
)
|
|
21
33
|
self.organization = organization
|
|
22
|
-
self.auth_header = {"Authorization": "Bearer %s" % (token,)}
|
|
23
34
|
self.team_members: dict[str, Any] = {}
|
|
24
|
-
self.api_url = f"https://{base_url}/api/v1"
|
|
25
|
-
|
|
26
|
-
self._timeout = timeout
|
|
27
|
-
"""Timeout to use for HTTP calls to Quay (seconds)."""
|
|
28
35
|
|
|
29
36
|
def list_team_members(self, team: str, **kwargs: Any) -> list[dict]:
|
|
30
37
|
"""
|
|
@@ -38,19 +45,20 @@ class QuayApi:
|
|
|
38
45
|
if cache_members:
|
|
39
46
|
return cache_members
|
|
40
47
|
|
|
41
|
-
url = f"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
url = f"/api/v1/organization/{self.organization}/team/{team}/members"
|
|
49
|
+
params = {"includePending": "true"}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
body = self._get(url, params=params)
|
|
53
|
+
except requests.exceptions.HTTPError as e:
|
|
54
|
+
if e.response.status_code == 404:
|
|
55
|
+
raise QuayTeamNotFoundError(
|
|
56
|
+
f"team {team} is not found in "
|
|
57
|
+
f"org {self.organization}. "
|
|
58
|
+
f"contact org owner to create the "
|
|
59
|
+
f"team manually."
|
|
60
|
+
) from e
|
|
61
|
+
raise
|
|
54
62
|
|
|
55
63
|
# Using a set because members may be repeated
|
|
56
64
|
members = {member["name"] for member in body["members"]}
|
|
@@ -61,30 +69,37 @@ class QuayApi:
|
|
|
61
69
|
return members_list
|
|
62
70
|
|
|
63
71
|
def user_exists(self, user: str) -> bool:
|
|
64
|
-
url = f"
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
url = f"/api/v1/users/{user}"
|
|
73
|
+
try:
|
|
74
|
+
self._get(url)
|
|
75
|
+
return True
|
|
76
|
+
except requests.exceptions.HTTPError:
|
|
77
|
+
return False
|
|
67
78
|
|
|
68
79
|
def remove_user_from_team(self, user: str, team: str) -> bool:
|
|
69
80
|
"""Deletes an user from a team.
|
|
70
81
|
|
|
71
82
|
:raises HTTPError if there are any problems with the request
|
|
72
83
|
"""
|
|
73
|
-
url_team =
|
|
84
|
+
url_team = (
|
|
85
|
+
f"/api/v1/organization/{self.organization}/team/{team}/members/{user}"
|
|
86
|
+
)
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
try:
|
|
89
|
+
self._delete(url_team)
|
|
90
|
+
except requests.exceptions.HTTPError as e:
|
|
91
|
+
message = ""
|
|
92
|
+
if e.response is not None:
|
|
93
|
+
with contextlib.suppress(ValueError, AttributeError):
|
|
94
|
+
message = e.response.json().get("message", "")
|
|
78
95
|
|
|
79
96
|
expected_message = f"User {user} does not belong to team {team}"
|
|
80
97
|
|
|
81
98
|
if message != expected_message:
|
|
82
|
-
|
|
99
|
+
raise
|
|
83
100
|
|
|
84
|
-
url_org = f"
|
|
85
|
-
|
|
86
|
-
r = requests.delete(url_org, headers=self.auth_header, timeout=self._timeout)
|
|
87
|
-
r.raise_for_status()
|
|
101
|
+
url_org = f"/api/v1/organization/{self.organization}/members/{user}"
|
|
102
|
+
self._delete(url_org)
|
|
88
103
|
|
|
89
104
|
return True
|
|
90
105
|
|
|
@@ -96,9 +111,8 @@ class QuayApi:
|
|
|
96
111
|
if user in self.list_team_members(team, cache=True):
|
|
97
112
|
return True
|
|
98
113
|
|
|
99
|
-
url = f"
|
|
100
|
-
|
|
101
|
-
r.raise_for_status()
|
|
114
|
+
url = f"/api/v1/organization/{self.organization}/team/{team}/members/{user}"
|
|
115
|
+
self._put(url)
|
|
102
116
|
return True
|
|
103
117
|
|
|
104
118
|
def create_or_update_team(
|
|
@@ -115,17 +129,14 @@ class QuayApi:
|
|
|
115
129
|
:raises HTTPError: unsuccessful attempt to create the team
|
|
116
130
|
"""
|
|
117
131
|
|
|
118
|
-
url = f"
|
|
132
|
+
url = f"/api/v1/organization/{self.organization}/team/{team}"
|
|
119
133
|
|
|
120
134
|
payload = {"role": role}
|
|
121
135
|
|
|
122
136
|
if description:
|
|
123
137
|
payload.update({"description": description})
|
|
124
138
|
|
|
125
|
-
|
|
126
|
-
url, headers=self.auth_header, json=payload, timeout=self._timeout
|
|
127
|
-
)
|
|
128
|
-
r.raise_for_status()
|
|
139
|
+
self._put(url, data=payload)
|
|
129
140
|
|
|
130
141
|
def list_images(
|
|
131
142
|
self, images: list | None = None, page: str | None = None, count: int = 0
|
|
@@ -140,7 +151,7 @@ class QuayApi:
|
|
|
140
151
|
if count > self.LIMIT_FOLLOWS:
|
|
141
152
|
raise ValueError("Too many page follows")
|
|
142
153
|
|
|
143
|
-
url =
|
|
154
|
+
url = "/api/v1/repository"
|
|
144
155
|
|
|
145
156
|
# params
|
|
146
157
|
params = {"namespace": self.organization}
|
|
@@ -148,13 +159,7 @@ class QuayApi:
|
|
|
148
159
|
params["next_page"] = page
|
|
149
160
|
|
|
150
161
|
# perform request
|
|
151
|
-
|
|
152
|
-
url, params=params, headers=self.auth_header, timeout=self._timeout
|
|
153
|
-
)
|
|
154
|
-
r.raise_for_status()
|
|
155
|
-
|
|
156
|
-
# read body
|
|
157
|
-
body = r.json()
|
|
162
|
+
body = self._get(url, params=params)
|
|
158
163
|
repositories = body.get("repositories", [])
|
|
159
164
|
next_page = body.get("next_page")
|
|
160
165
|
|
|
@@ -176,7 +181,7 @@ class QuayApi:
|
|
|
176
181
|
"""
|
|
177
182
|
visibility = "public" if public else "private"
|
|
178
183
|
|
|
179
|
-
url =
|
|
184
|
+
url = "/repository"
|
|
180
185
|
|
|
181
186
|
params = {
|
|
182
187
|
"repo_kind": "image",
|
|
@@ -186,29 +191,16 @@ class QuayApi:
|
|
|
186
191
|
"description": description,
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
|
|
190
|
-
r = requests.post(
|
|
191
|
-
url, json=params, headers=self.auth_header, timeout=self._timeout
|
|
192
|
-
)
|
|
193
|
-
r.raise_for_status()
|
|
194
|
+
self._post(url, data=params)
|
|
194
195
|
|
|
195
196
|
def repo_delete(self, repo_name: str) -> None:
|
|
196
|
-
url = f"
|
|
197
|
-
|
|
198
|
-
# perform request
|
|
199
|
-
r = requests.delete(url, headers=self.auth_header, timeout=self._timeout)
|
|
200
|
-
r.raise_for_status()
|
|
197
|
+
url = f"/api/v1/repository/{self.organization}/{repo_name}"
|
|
198
|
+
self._delete(url)
|
|
201
199
|
|
|
202
200
|
def repo_update_description(self, repo_name: str, description: str) -> None:
|
|
203
|
-
url = f"
|
|
204
|
-
|
|
201
|
+
url = f"/api/v1/repository/{self.organization}/{repo_name}"
|
|
205
202
|
params = {"description": description}
|
|
206
|
-
|
|
207
|
-
# perform request
|
|
208
|
-
r = requests.put(
|
|
209
|
-
url, json=params, headers=self.auth_header, timeout=self._timeout
|
|
210
|
-
)
|
|
211
|
-
r.raise_for_status()
|
|
203
|
+
self._put(url, data=params)
|
|
212
204
|
|
|
213
205
|
def repo_make_public(self, repo_name: str) -> None:
|
|
214
206
|
self._repo_change_visibility(repo_name, "public")
|
|
@@ -217,39 +209,34 @@ class QuayApi:
|
|
|
217
209
|
self._repo_change_visibility(repo_name, "private")
|
|
218
210
|
|
|
219
211
|
def _repo_change_visibility(self, repo_name: str, visibility: str) -> None:
|
|
220
|
-
url = f"
|
|
221
|
-
|
|
212
|
+
url = f"/api/v1/repository/{self.organization}/{repo_name}/changevisibility"
|
|
222
213
|
params = {"visibility": visibility}
|
|
223
|
-
|
|
224
|
-
# perform request
|
|
225
|
-
r = requests.post(
|
|
226
|
-
url, json=params, headers=self.auth_header, timeout=self._timeout
|
|
227
|
-
)
|
|
228
|
-
r.raise_for_status()
|
|
214
|
+
self._post(url, data=params)
|
|
229
215
|
|
|
230
216
|
def get_repo_team_permissions(self, repo_name: str, team: str) -> str | None:
|
|
231
217
|
url = (
|
|
232
|
-
f"
|
|
218
|
+
f"/api/v1/repository/{self.organization}/"
|
|
233
219
|
+ f"{repo_name}/permissions/team/{team}"
|
|
234
220
|
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
221
|
+
try:
|
|
222
|
+
body = self._get(url)
|
|
223
|
+
return body.get("role") or None
|
|
224
|
+
except requests.exceptions.HTTPError as e:
|
|
225
|
+
message = ""
|
|
226
|
+
if e.response is not None:
|
|
227
|
+
with contextlib.suppress(ValueError, AttributeError):
|
|
228
|
+
message = e.response.json().get("message", "")
|
|
229
|
+
|
|
238
230
|
expected_message = "Team does not have permission for repo."
|
|
239
231
|
if message == expected_message:
|
|
240
232
|
return None
|
|
241
233
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return r.json().get("role") or None
|
|
234
|
+
raise
|
|
245
235
|
|
|
246
236
|
def set_repo_team_permissions(self, repo_name: str, team: str, role: str) -> None:
|
|
247
237
|
url = (
|
|
248
|
-
f"
|
|
238
|
+
f"/api/v1/repository/{self.organization}/"
|
|
249
239
|
+ f"{repo_name}/permissions/team/{team}"
|
|
250
240
|
)
|
|
251
241
|
body = {"role": role}
|
|
252
|
-
|
|
253
|
-
url, json=body, headers=self.auth_header, timeout=self._timeout
|
|
254
|
-
)
|
|
255
|
-
r.raise_for_status()
|
|
242
|
+
self._put(url, data=body)
|
{qontract_reconcile-0.10.2.dev456.dist-info → qontract_reconcile-0.10.2.dev473.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|