Skip to content

Commit c6cfab2

Browse files
committed
test(evalhub): add multi-tenancy integration tests
Add 24 integration tests for EvalHub covering deployment topology, health, multi-tenancy authorisation (providers, collections, jobs), job lifecycle with vLLM emulator, and operator RBAC provisioning. Signed-off-by: Rui Vieira <ruidevieira@googlemail.com>
1 parent f8c8a5a commit c6cfab2

File tree

7 files changed

+313
-23
lines changed

7 files changed

+313
-23
lines changed

renovate.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@
5959
"schedule": [
6060
"before 6pm on wednesday"
6161
],
62-
"prPriority": 5,
63-
"pinDigests": true
62+
"prPriority": 5
63+
},
64+
{
65+
"matchDatasources": ["docker"],
66+
"matchPackageNames": ["quay.io/fedora/fedora"],
67+
"enabled": false
6468
}
6569
]
6670
}

tests/model_explainability/evalhub/multitenancy/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ def tenant_a_token(
255255

256256

257257
@pytest.fixture(scope="class")
258-
def vllm_emulator_deployment(
258+
def evalhub_vllm_emulator_deployment(
259259
admin_client: DynamicClient,
260260
tenant_a_namespace: Namespace,
261261
tenant_a_rbac_ready: None,
@@ -298,10 +298,10 @@ def vllm_emulator_deployment(
298298

299299

300300
@pytest.fixture(scope="class")
301-
def vllm_emulator_service(
301+
def evalhub_vllm_emulator_service(
302302
admin_client: DynamicClient,
303303
tenant_a_namespace: Namespace,
304-
vllm_emulator_deployment: Deployment,
304+
evalhub_vllm_emulator_deployment: Deployment,
305305
) -> Generator[Service, Any, Any]:
306306
"""Service fronting the vLLM emulator in tenant-a."""
307307
with Service(

tests/model_explainability/evalhub/multitenancy/test_evalhub_job_delete_mt.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ def test_job_delete_authorized_tenant(
3737
tenant_a_namespace: Namespace,
3838
evalhub_mt_ca_bundle_file: str,
3939
evalhub_mt_route: Route,
40-
vllm_emulator_service: Service,
40+
evalhub_vllm_emulator_service: Service,
4141
) -> None:
4242
"""User with evaluations-delete RBAC in tenant-a can delete a job."""
4343
payload = build_evalhub_job_payload(
44-
model_service_name=vllm_emulator_service.name,
44+
model_service_name=evalhub_vllm_emulator_service.name,
4545
tenant_namespace=tenant_a_namespace.name,
4646
job_name="evalhub-delete-test-job",
4747
)
@@ -70,12 +70,12 @@ def test_job_delete_cross_tenant_denied(
7070
tenant_b_namespace: Namespace,
7171
evalhub_mt_ca_bundle_file: str,
7272
evalhub_mt_route: Route,
73-
vllm_emulator_service: Service,
73+
evalhub_vllm_emulator_service: Service,
7474
) -> None:
7575
"""User with RBAC in tenant-a is denied job deletion for tenant-b."""
7676
# Submit in tenant-a first to get a valid job ID
7777
payload = build_evalhub_job_payload(
78-
model_service_name=vllm_emulator_service.name,
78+
model_service_name=evalhub_vllm_emulator_service.name,
7979
tenant_namespace=tenant_a_namespace.name,
8080
job_name="evalhub-delete-test-job",
8181
)
@@ -103,12 +103,12 @@ def test_job_delete_missing_tenant_rejected(
103103
tenant_a_namespace: Namespace,
104104
evalhub_mt_ca_bundle_file: str,
105105
evalhub_mt_route: Route,
106-
vllm_emulator_service: Service,
106+
evalhub_vllm_emulator_service: Service,
107107
) -> None:
108108
"""Job deletion without X-Tenant header is rejected with 400."""
109109
# Submit in tenant-a first to get a valid job ID
110110
payload = build_evalhub_job_payload(
111-
model_service_name=vllm_emulator_service.name,
111+
model_service_name=evalhub_vllm_emulator_service.name,
112112
tenant_namespace=tenant_a_namespace.name,
113113
job_name="evalhub-delete-test-job",
114114
)

tests/model_explainability/evalhub/multitenancy/test_evalhub_jobs_mt.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ def test_job_submit_authorized_tenant(
4343
tenant_a_namespace: Namespace,
4444
evalhub_mt_ca_bundle_file: str,
4545
evalhub_mt_route: Route,
46-
vllm_emulator_service: Service,
46+
evalhub_vllm_emulator_service: Service,
4747
) -> None:
4848
"""User with evaluations-create RBAC in tenant-a can submit a job."""
4949
payload = build_evalhub_job_payload(
50-
model_service_name=vllm_emulator_service.name,
50+
model_service_name=evalhub_vllm_emulator_service.name,
5151
tenant_namespace=tenant_a_namespace.name,
5252
)
5353
data = submit_evalhub_job(
@@ -69,11 +69,11 @@ def test_job_completion_and_results(
6969
tenant_a_namespace: Namespace,
7070
evalhub_mt_ca_bundle_file: str,
7171
evalhub_mt_route: Route,
72-
vllm_emulator_service: Service,
72+
evalhub_vllm_emulator_service: Service,
7373
) -> None:
7474
"""Submit a job and wait for it to complete with benchmark results."""
7575
payload = build_evalhub_job_payload(
76-
model_service_name=vllm_emulator_service.name,
76+
model_service_name=evalhub_vllm_emulator_service.name,
7777
tenant_namespace=tenant_a_namespace.name,
7878
)
7979
data = submit_evalhub_job(
@@ -102,11 +102,11 @@ def test_job_pod_reaches_succeeded(
102102
tenant_a_namespace: Namespace,
103103
evalhub_mt_ca_bundle_file: str,
104104
evalhub_mt_route: Route,
105-
vllm_emulator_service: Service,
105+
evalhub_vllm_emulator_service: Service,
106106
) -> None:
107107
"""After job completion, the K8s pod should be in Succeeded state."""
108108
payload = build_evalhub_job_payload(
109-
model_service_name=vllm_emulator_service.name,
109+
model_service_name=evalhub_vllm_emulator_service.name,
110110
tenant_namespace=tenant_a_namespace.name,
111111
)
112112
data = submit_evalhub_job(
@@ -150,11 +150,11 @@ def test_job_submit_cross_tenant_denied(
150150
tenant_b_namespace: Namespace,
151151
evalhub_mt_ca_bundle_file: str,
152152
evalhub_mt_route: Route,
153-
vllm_emulator_service: Service,
153+
evalhub_vllm_emulator_service: Service,
154154
) -> None:
155155
"""User with RBAC in tenant-a is denied job submission for tenant-b."""
156156
payload = build_evalhub_job_payload(
157-
model_service_name=vllm_emulator_service.name,
157+
model_service_name=evalhub_vllm_emulator_service.name,
158158
tenant_namespace=tenant_a_namespace.name,
159159
)
160160
validate_evalhub_post_denied(
@@ -172,11 +172,11 @@ def test_job_submit_missing_tenant_rejected(
172172
tenant_a_namespace: Namespace,
173173
evalhub_mt_ca_bundle_file: str,
174174
evalhub_mt_route: Route,
175-
vllm_emulator_service: Service,
175+
evalhub_vllm_emulator_service: Service,
176176
) -> None:
177177
"""Job submission without X-Tenant header is rejected with 400."""
178178
payload = build_evalhub_job_payload(
179-
model_service_name=vllm_emulator_service.name,
179+
model_service_name=evalhub_vllm_emulator_service.name,
180180
tenant_namespace=tenant_a_namespace.name,
181181
)
182182
validate_evalhub_post_no_tenant(
@@ -193,11 +193,11 @@ def test_job_list_authorized_tenant(
193193
tenant_a_namespace: Namespace,
194194
evalhub_mt_ca_bundle_file: str,
195195
evalhub_mt_route: Route,
196-
vllm_emulator_service: Service,
196+
evalhub_vllm_emulator_service: Service,
197197
) -> None:
198198
"""After submitting a job, listing jobs for tenant-a shows it."""
199199
payload = build_evalhub_job_payload(
200-
model_service_name=vllm_emulator_service.name,
200+
model_service_name=evalhub_vllm_emulator_service.name,
201201
tenant_namespace=tenant_a_namespace.name,
202202
)
203203
submitted = submit_evalhub_job(

tests/model_serving/maas_billing/maas_subscription/conftest.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@
55
import requests
66
import structlog
77
from kubernetes.dynamic import DynamicClient
8+
from ocp_resources.cron_job import CronJob
89
from ocp_resources.llm_inference_service import LLMInferenceService
910
from ocp_resources.maas_auth_policy import MaaSAuthPolicy
1011
from ocp_resources.maas_model_ref import MaaSModelRef
1112
from ocp_resources.maas_subscription import MaaSSubscription
1213
from ocp_resources.namespace import Namespace
14+
from ocp_resources.network_policy import NetworkPolicy
15+
from ocp_resources.pod import Pod
1316
from ocp_resources.resource import ResourceEditor
1417
from ocp_resources.service_account import ServiceAccount
1518
from pytest_testconfig import config as py_config
1619

1720
from tests.model_serving.maas_billing.maas_subscription.utils import (
21+
assert_api_key_created_ok,
1822
create_and_yield_api_key_id,
1923
create_api_key,
2024
create_maas_subscription,
25+
get_maas_api_labels,
2126
patch_llmisvc_with_maas_router_and_tiers,
2227
resolve_api_key_username,
2328
revoke_api_key,
@@ -786,3 +791,87 @@ def short_expiration_api_key_id(
786791
key_name_prefix="e2e-exp-short",
787792
expires_in="1h",
788793
)
794+
795+
796+
@pytest.fixture()
797+
def maas_cleanup_cronjob(
798+
admin_client: DynamicClient,
799+
) -> CronJob:
800+
"""Return the maas-api-key-cleanup CronJob, asserting it exists."""
801+
applications_namespace = py_config["applications_namespace"]
802+
cronjob = CronJob(
803+
client=admin_client,
804+
name="maas-api-key-cleanup",
805+
namespace=applications_namespace,
806+
)
807+
assert cronjob.exists, f"CronJob maas-api-key-cleanup not found in {applications_namespace}"
808+
return cronjob
809+
810+
811+
@pytest.fixture()
812+
def maas_cleanup_networkpolicy(
813+
admin_client: DynamicClient,
814+
) -> NetworkPolicy:
815+
"""Return the maas-api-cleanup-restrict NetworkPolicy, asserting it exists."""
816+
applications_namespace = py_config["applications_namespace"]
817+
network_policy = NetworkPolicy(
818+
client=admin_client,
819+
name="maas-api-cleanup-restrict",
820+
namespace=applications_namespace,
821+
)
822+
assert network_policy.exists, f"NetworkPolicy maas-api-cleanup-restrict not found in {applications_namespace}"
823+
return network_policy
824+
825+
826+
@pytest.fixture()
827+
def maas_api_pod_name(
828+
admin_client: DynamicClient,
829+
) -> str:
830+
"""Return the name of the single running maas-api pod (exactly one pod is expected)."""
831+
applications_namespace = py_config["applications_namespace"]
832+
label_selector = ",".join(f"{k}={v}" for k, v in get_maas_api_labels().items())
833+
pods = list(
834+
Pod.get(
835+
client=admin_client,
836+
namespace=applications_namespace,
837+
label_selector=label_selector,
838+
)
839+
)
840+
assert len(pods) == 1, f"Expected exactly 1 maas-api pod in {applications_namespace}, found {len(pods)}"
841+
assert pods[0].instance.status.phase == "Running", (
842+
f"maas-api pod '{pods[0].name}' is not Running (phase={pods[0].instance.status.phase})"
843+
)
844+
return pods[0].name
845+
846+
847+
@pytest.fixture()
848+
def ephemeral_api_key(
849+
request_session_http: requests.Session,
850+
base_url: str,
851+
ocp_token_for_actor: str,
852+
) -> Generator[dict[str, Any]]:
853+
"""Create an ephemeral API key and revoke it on teardown."""
854+
creation_response, api_key_data = create_api_key(
855+
base_url=base_url,
856+
ocp_user_token=ocp_token_for_actor,
857+
request_session_http=request_session_http,
858+
api_key_name=f"e2e-ephemeral-{generate_random_name()}",
859+
expires_in="1h",
860+
ephemeral=True,
861+
raise_on_error=False,
862+
)
863+
assert_api_key_created_ok(resp=creation_response, body=api_key_data, required_fields=("key", "id"))
864+
LOGGER.info(
865+
f"[ephemeral] Created ephemeral key: id={api_key_data['id']}, expiresAt={api_key_data.get('expiresAt')}"
866+
)
867+
yield api_key_data
868+
revoke_response, _ = revoke_api_key(
869+
request_session_http=request_session_http,
870+
base_url=base_url,
871+
key_id=api_key_data["id"],
872+
ocp_user_token=ocp_token_for_actor,
873+
)
874+
if revoke_response.status_code not in (200, 404):
875+
raise AssertionError(
876+
f"Unexpected teardown status for ephemeral key id={api_key_data['id']}: {revoke_response.status_code}"
877+
)

0 commit comments

Comments
 (0)