|
4 | 4 | import pytest |
5 | 5 | import requests |
6 | 6 | from simple_logger.logger import get_logger |
7 | | -from utilities.plugins.constant import RestHeader, OpenAIEnpoints |
| 7 | +from utilities.plugins.constant import OpenAIEnpoints |
8 | 8 |
|
9 | 9 | from kubernetes.dynamic import DynamicClient |
10 | 10 | from ocp_resources.infrastructure import Infrastructure |
|
15 | 15 | from utilities.infra import login_with_user_password, get_openshift_token |
16 | 16 | from utilities.general import wait_for_oauth_openshift_deployment |
17 | 17 | from ocp_resources.secret import Secret |
18 | | -from timeout_sampler import TimeoutExpiredError |
19 | | - |
20 | | - |
21 | 18 | from tests.model_serving.model_server.maas_billing.utils import ( |
22 | 19 | detect_scheme_via_llmisvc, |
23 | 20 | host_from_ingress_domain, |
24 | 21 | mint_token, |
25 | 22 | llmis_name, |
26 | 23 | create_maas_group, |
| 24 | + build_maas_headers, |
| 25 | + get_maas_models_response, |
27 | 26 | ) |
28 | 27 |
|
29 | 28 |
|
@@ -53,7 +52,7 @@ def minted_token(request_session_http, base_url: str, current_client_token: str) |
53 | 52 | minutes=30, |
54 | 53 | http_session=request_session_http, |
55 | 54 | ) |
56 | | - LOGGER.info("Mint token response status=%s", resp.status_code) |
| 55 | + LOGGER.info(f"Mint token response status={resp.status_code}") |
57 | 56 | assert resp.status_code in (200, 201), f"mint failed: {resp.status_code} {resp.text[:200]}" |
58 | 57 | token = body.get("token", "") |
59 | 58 | assert isinstance(token, str) and len(token) > 10, f"no usable token in response: {body}" |
@@ -81,22 +80,20 @@ def model_url(admin_client) -> str: |
81 | 80 |
|
82 | 81 | @pytest.fixture |
83 | 82 | def maas_headers(minted_token: str) -> dict: |
84 | | - """Common headers for MaaS API calls.""" |
85 | | - return {"Authorization": f"Bearer {minted_token}", **RestHeader.HEADERS} |
| 83 | + return build_maas_headers(token=minted_token) |
86 | 84 |
|
87 | 85 |
|
88 | 86 | @pytest.fixture |
89 | 87 | def maas_models( |
90 | | - request_session_http: requests.Session, |
91 | | - base_url: str, |
92 | | - maas_headers: dict, |
| 88 | + request_session_http, |
| 89 | + base_url, |
| 90 | + maas_headers, |
93 | 91 | ): |
94 | | - """ |
95 | | - Call /v1/models once and return the list of models. |
96 | | -
|
97 | | - """ |
98 | | - models_url = f"{base_url}{MODELS_INFO}" |
99 | | - resp = request_session_http.get(models_url, headers=maas_headers, timeout=60) |
| 92 | + resp = get_maas_models_response( |
| 93 | + session=request_session_http, |
| 94 | + base_url=base_url, |
| 95 | + headers=maas_headers, |
| 96 | + ) |
100 | 97 |
|
101 | 98 | assert resp.status_code == 200, f"/v1/models failed: {resp.status_code} {resp.text[:200]}" |
102 | 99 |
|
@@ -133,113 +130,129 @@ def maas_user_credentials_both() -> dict[str, str]: |
133 | 130 |
|
134 | 131 |
|
135 | 132 | @pytest.fixture(scope="session") |
136 | | -def maas_rbac_idp_env( |
137 | | - admin_client: DynamicClient, |
| 133 | +def maas_htpasswd_files( |
138 | 134 | maas_user_credentials_both: dict[str, str], |
139 | | - is_byoidc: bool, |
140 | | -) -> Generator[dict[str, str], None, None]: |
141 | | - """ |
142 | | - - Creates a single htpasswd Secret with FREE + PREMIUM users. |
143 | | - - Adds a temporary MaaS HTPasswd IDP to oauth/cluster using ResourceEditor. |
144 | | - - Waits for oauth-openshift rollout after patch. |
145 | | - - On teardown, ResourceEditor restores the original OAuth spec and we wait again. |
146 | | - - Deletes the temporary htpasswd Secret. |
| 135 | +) -> Generator[tuple[str, str, str, str], None, None]: |
147 | 136 | """ |
148 | | - if is_byoidc: |
149 | | - pytest.skip("Working on OIDC support for tests that use htpasswd IDP for MaaS") |
| 137 | + Create per-user htpasswd files for FREE and PREMIUM users and return |
| 138 | + their file paths + base64 contents. |
150 | 139 |
|
| 140 | + Cleanup of the temp files happens at teardown. |
| 141 | + """ |
151 | 142 | free_username = maas_user_credentials_both["free_user"] |
152 | 143 | free_password = maas_user_credentials_both["free_pass"] |
153 | 144 | premium_username = maas_user_credentials_both["premium_user"] |
154 | 145 | premium_password = maas_user_credentials_both["premium_pass"] |
155 | | - secret_name = maas_user_credentials_both["secret_name"] |
156 | | - idp_name = maas_user_credentials_both["idp_name"] |
157 | 146 |
|
158 | 147 | free_htpasswd_file_path, free_htpasswd_b64 = create_htpasswd_file( |
159 | 148 | username=free_username, |
160 | 149 | password=free_password, |
161 | 150 | ) |
162 | | - |
163 | 151 | premium_htpasswd_file_path, premium_htpasswd_b64 = create_htpasswd_file( |
164 | 152 | username=premium_username, |
165 | 153 | password=premium_password, |
166 | 154 | ) |
167 | 155 |
|
168 | 156 | try: |
169 | | - free_bytes = base64.b64decode(s=free_htpasswd_b64) |
170 | | - premium_bytes = base64.b64decode(s=premium_htpasswd_b64) |
171 | | - combined_bytes = free_bytes + b"\n" + premium_bytes |
172 | | - combined_htpasswd_b64 = base64.b64encode(s=combined_bytes).decode("utf-8") |
173 | | - |
174 | | - oauth_resource = OAuth(name="cluster", client=admin_client) |
175 | | - oauth_spec = getattr(oauth_resource.instance, "spec", {}) or {} |
176 | | - existing_identity_providers = oauth_spec.get("identityProviders") or [] |
177 | | - |
178 | | - maas_identity_provider = { |
179 | | - "name": idp_name, |
180 | | - "mappingMethod": "claim", |
181 | | - "type": "HTPasswd", |
182 | | - "challenge": True, |
183 | | - "login": True, |
184 | | - "htpasswd": {"fileData": {"name": secret_name}}, |
185 | | - } |
186 | | - |
187 | | - updated_identity_providers = existing_identity_providers + [maas_identity_provider] |
188 | | - |
189 | | - LOGGER.info( |
190 | | - f"MaaS RBAC: creating shared htpasswd Secret '{secret_name}' for users " |
191 | | - f"'{free_username}' and '{premium_username}'" |
| 157 | + yield ( |
| 158 | + free_htpasswd_file_path, |
| 159 | + free_htpasswd_b64, |
| 160 | + premium_htpasswd_file_path, |
| 161 | + premium_htpasswd_b64, |
192 | 162 | ) |
| 163 | + finally: |
| 164 | + free_htpasswd_file_path.unlink(missing_ok=True) |
| 165 | + premium_htpasswd_file_path.unlink(missing_ok=True) |
193 | 166 |
|
194 | | - # --- update OAuth + create Secret --- |
195 | | - with ( |
196 | | - Secret( |
197 | | - client=admin_client, |
198 | | - name=secret_name, |
199 | | - namespace="openshift-config", |
200 | | - htpasswd=combined_htpasswd_b64, |
201 | | - type="Opaque", |
202 | | - teardown=True, |
203 | | - wait_for_resource=True, |
204 | | - ), |
205 | | - ResourceEditor(patches={oauth_resource: {"spec": {"identityProviders": updated_identity_providers}}}), |
206 | | - ): |
207 | | - LOGGER.info(f"MaaS RBAC: updating OAuth with MaaS htpasswd IDP '{maas_identity_provider['name']}'") |
208 | | - |
209 | | - wait_for_oauth_openshift_deployment() |
210 | | - LOGGER.info(f"MaaS RBAC: OAuth updated with MaaS IDP '{maas_identity_provider['name']}'") |
211 | | - |
212 | | - # >>> this is the yield that pytest uses <<< |
213 | | - yield maas_user_credentials_both |
214 | | - |
215 | | - LOGGER.info("MaaS RBAC: restoring OAuth identityProviders to original state") |
216 | | - |
217 | | - # --- after exit: secret deleted + OAuth restored --- |
218 | | - try: |
219 | | - wait_for_oauth_openshift_deployment() |
220 | | - LOGGER.info("MaaS RBAC: oauth-openshift rollout completed after restoring OAuth.") |
221 | | - except TimeoutExpiredError as timeout_error: |
222 | | - LOGGER.warning( |
223 | | - f"MaaS RBAC: timeout while waiting for oauth-openshift rollout after restoring OAuth. " |
224 | | - f"Continuing teardown. Details: {timeout_error}" |
225 | | - ) |
226 | 167 |
|
227 | | - # --- final sanity check --- |
228 | | - oauth_resource_after = OAuth(name="cluster", client=admin_client) |
229 | | - oauth_spec_after = getattr(oauth_resource_after.instance, "spec", {}) or {} |
230 | | - current_idps = oauth_spec_after.get("identityProviders") or [] |
| 168 | +@pytest.fixture(scope="session") |
| 169 | +def maas_htpasswd_oauth_idp( |
| 170 | + admin_client: DynamicClient, |
| 171 | + maas_user_credentials_both: dict[str, str], |
| 172 | + maas_htpasswd_files: tuple[str, str, str, str], |
| 173 | + is_byoidc: bool, |
| 174 | +): |
| 175 | + """ |
| 176 | + - Combines FREE + PREMIUM htpasswd entries into a single Secret. |
| 177 | + - Adds the MaaS HTPasswd IDP to oauth/cluster using ResourceEditor. |
| 178 | + - Waits for oauth-openshift rollout after patch. |
| 179 | + - On teardown, waits again and verifies the IDP is gone. |
| 180 | + """ |
| 181 | + if is_byoidc: |
| 182 | + pytest.skip("Working on OIDC support for tests that use htpasswd IDP for MaaS") |
| 183 | + |
| 184 | + ( |
| 185 | + _free_htpasswd_file_path, |
| 186 | + free_htpasswd_b64, |
| 187 | + _premium_htpasswd_file_path, |
| 188 | + premium_htpasswd_b64, |
| 189 | + ) = maas_htpasswd_files |
231 | 190 |
|
232 | | - if any(idp.get("name") == idp_name for idp in current_idps): |
233 | | - LOGGER.warning( |
234 | | - f"MaaS RBAC: temporary MaaS IDP '{idp_name}' is STILL present in OAuth spec " |
235 | | - f"after teardown — please investigate cluster OAuth configuration." |
236 | | - ) |
237 | | - else: |
238 | | - LOGGER.info("MaaS RBAC: OAuth identityProviders restoration & cleanup completed") |
| 191 | + free_username = maas_user_credentials_both["free_user"] |
| 192 | + premium_username = maas_user_credentials_both["premium_user"] |
| 193 | + secret_name = maas_user_credentials_both["secret_name"] |
| 194 | + idp_name = maas_user_credentials_both["idp_name"] |
239 | 195 |
|
240 | | - finally: |
241 | | - free_htpasswd_file_path.unlink(missing_ok=True) |
242 | | - premium_htpasswd_file_path.unlink(missing_ok=True) |
| 196 | + free_bytes = base64.b64decode(s=free_htpasswd_b64) |
| 197 | + premium_bytes = base64.b64decode(s=premium_htpasswd_b64) |
| 198 | + combined_bytes = free_bytes + b"\n" + premium_bytes |
| 199 | + combined_htpasswd_b64 = base64.b64encode(s=combined_bytes).decode("utf-8") |
| 200 | + |
| 201 | + oauth_resource = OAuth(name="cluster", client=admin_client) |
| 202 | + oauth_spec = getattr(oauth_resource.instance, "spec", {}) or {} |
| 203 | + existing_idps = oauth_spec.get("identityProviders") or [] |
| 204 | + |
| 205 | + maas_idp = { |
| 206 | + "name": idp_name, |
| 207 | + "mappingMethod": "claim", |
| 208 | + "type": "HTPasswd", |
| 209 | + "challenge": True, |
| 210 | + "login": True, |
| 211 | + "htpasswd": {"fileData": {"name": secret_name}}, |
| 212 | + } |
| 213 | + |
| 214 | + updated_idps = existing_idps + [maas_idp] |
| 215 | + |
| 216 | + LOGGER.info( |
| 217 | + f"MaaS RBAC: creating shared htpasswd Secret '{secret_name}' " |
| 218 | + f"for users '{free_username}' and '{premium_username}'" |
| 219 | + ) |
| 220 | + |
| 221 | + with ( |
| 222 | + Secret( |
| 223 | + client=admin_client, |
| 224 | + name=secret_name, |
| 225 | + namespace="openshift-config", |
| 226 | + htpasswd=combined_htpasswd_b64, |
| 227 | + type="Opaque", |
| 228 | + teardown=True, |
| 229 | + wait_for_resource=True, |
| 230 | + ), |
| 231 | + ResourceEditor(patches={oauth_resource: {"spec": {"identityProviders": updated_idps}}}), |
| 232 | + ): |
| 233 | + LOGGER.info(f"MaaS RBAC: updating OAuth with MaaS htpasswd IDP '{maas_idp['name']}'") |
| 234 | + wait_for_oauth_openshift_deployment() |
| 235 | + LOGGER.info(f"MaaS RBAC: OAuth updated with MaaS IDP '{maas_idp['name']}'") |
| 236 | + yield |
| 237 | + |
| 238 | + # teardown checks |
| 239 | + wait_for_oauth_openshift_deployment() |
| 240 | + |
| 241 | + oauth_after = OAuth(name="cluster", client=admin_client) |
| 242 | + idps_after = getattr(oauth_after.instance, "spec", {}).get("identityProviders") or [] |
| 243 | + if any(idp.get("name") == idp_name for idp in idps_after): |
| 244 | + pytest.fail(f"MaaS RBAC: cleanup failed, IDP {idp_name} still present after teardown") |
| 245 | + |
| 246 | + LOGGER.info("MaaS RBAC: OAuth identityProviders restoration & cleanup completed") |
| 247 | + |
| 248 | + |
| 249 | +@pytest.fixture(scope="session") |
| 250 | +def maas_rbac_idp_env( |
| 251 | + maas_htpasswd_oauth_idp, |
| 252 | + maas_user_credentials_both: dict[str, str], |
| 253 | +): |
| 254 | + |
| 255 | + return maas_user_credentials_both |
243 | 256 |
|
244 | 257 |
|
245 | 258 | @pytest.fixture(scope="session") |
@@ -289,7 +302,7 @@ def maas_premium_user_session( |
289 | 302 | original_user: str, |
290 | 303 | maas_api_server_url: str, |
291 | 304 | is_byoidc: bool, |
292 | | - maas_rbac_idp_env: dict[str, str], # <-- same outer fixture |
| 305 | + maas_rbac_idp_env: dict[str, str], |
293 | 306 | ) -> Generator[UserTestSession, None, None]: |
294 | 307 | if is_byoidc: |
295 | 308 | pytest.skip("Working on OIDC support for tests that use htpasswd IDP for MaaS") |
@@ -368,27 +381,26 @@ def ocp_token_for_actor( |
368 | 381 | """ |
369 | 382 | Log in as the requested actor ('admin' / 'free' / 'premium') |
370 | 383 | and yield the OpenShift token for that user. |
371 | | -
|
372 | 384 | """ |
373 | 385 | actor_param = getattr(request, "param", None) |
374 | 386 |
|
375 | 387 | if isinstance(actor_param, dict): |
376 | | - actor_kind = actor_param.get("kind", "admin") |
| 388 | + actor_type = actor_param.get("type", "admin") |
377 | 389 | else: |
378 | | - actor_kind = actor_param or "admin" |
| 390 | + actor_type = actor_param or "admin" |
379 | 391 |
|
380 | | - if actor_kind == "admin": |
| 392 | + if actor_type == "admin": |
381 | 393 | LOGGER.info("MaaS RBAC: using existing admin session to obtain token") |
382 | 394 | yield get_openshift_token(client=admin_client) |
383 | 395 | else: |
384 | | - if actor_kind == "free": |
| 396 | + if actor_type == "free": |
385 | 397 | user_session = maas_free_user_session |
386 | | - elif actor_kind == "premium": |
| 398 | + elif actor_type == "premium": |
387 | 399 | user_session = maas_premium_user_session |
388 | 400 | else: |
389 | | - raise ValueError(f"Unknown actor kind: {actor_kind!r}") |
| 401 | + raise ValueError(f"Unknown actor type: {actor_type!r}") |
390 | 402 |
|
391 | | - LOGGER.info(f"MaaS RBAC: logging in as MaaS {actor_kind} user '{user_session.username}'") |
| 403 | + LOGGER.info(f"MaaS RBAC: logging in as MaaS {actor_type} user '{user_session.username}'") |
392 | 404 | login_successful = login_with_user_password( |
393 | 405 | api_address=maas_api_server_url, |
394 | 406 | user=user_session.username, |
@@ -426,11 +438,31 @@ def maas_token_for_actor( |
426 | 438 | http_session=request_session_http, |
427 | 439 | minutes=30, |
428 | 440 | ) |
429 | | - LOGGER.info("MaaS RBAC: mint token status=%s", response.status_code) |
| 441 | + LOGGER.info(f"MaaS RBAC: mint token status={response.status_code}") |
430 | 442 | assert response.status_code in (200, 201), f"mint failed: {response.status_code} {response.text[:200]}" |
431 | 443 |
|
432 | 444 | token = body.get("token", "") |
433 | 445 | assert isinstance(token, str) and len(token) > 10, "no usable MaaS token in response" |
434 | 446 |
|
435 | | - LOGGER.info("MaaS RBAC: minted MaaS token len=%s for current actor", len(token)) |
| 447 | + LOGGER.info(f"MaaS RBAC: minted MaaS token len={len(token)} for current actor") |
436 | 448 | return token |
| 449 | + |
| 450 | + |
| 451 | +@pytest.fixture |
| 452 | +def maas_headers_for_actor(maas_token_for_actor: str) -> dict: |
| 453 | + """Headers for the current actor (admin/free/premium).""" |
| 454 | + return build_maas_headers(token=maas_token_for_actor) |
| 455 | + |
| 456 | + |
| 457 | +@pytest.fixture |
| 458 | +def maas_models_response_for_actor( |
| 459 | + request_session_http: requests.Session, |
| 460 | + base_url: str, |
| 461 | + maas_headers_for_actor: dict, |
| 462 | +): |
| 463 | + """Raw /v1/models response for the current actor.""" |
| 464 | + return get_maas_models_response( |
| 465 | + session=request_session_http, |
| 466 | + base_url=base_url, |
| 467 | + headers=maas_headers_for_actor, |
| 468 | + ) |
0 commit comments