Skip to content

Commit d476993

Browse files
authored
test: Add deployment tests for registered models (#1174)
* test: Add deployment tests for registered models * fix: review comments
1 parent 6d3f00a commit d476993

File tree

3 files changed

+396
-1
lines changed

3 files changed

+396
-1
lines changed

tests/model_registry/model_registry/rest_api/conftest.py

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
import base64
12
import copy
23
import os
34
import tempfile
45
from collections.abc import Generator
56
from typing import Any
67

8+
import portforward
79
import pytest
810
from kubernetes.dynamic import DynamicClient
911
from ocp_resources.config_map import ConfigMap
1012
from ocp_resources.deployment import Deployment
13+
from ocp_resources.inference_service import InferenceService
14+
from ocp_resources.namespace import Namespace
15+
from ocp_resources.pod import Pod
1116
from ocp_resources.resource import ResourceEditor
1217
from ocp_resources.secret import Secret
18+
from ocp_resources.serving_runtime import ServingRuntime
1319
from pytest_testconfig import config as py_config
1420
from simple_logger.logger import get_logger
1521

@@ -38,6 +44,8 @@
3844
from utilities.certificates_utils import create_ca_bundle_with_router_cert, create_k8s_secret
3945
from utilities.exceptions import MissingParameter
4046
from utilities.general import generate_random_name, wait_for_pods_running
47+
from utilities.infra import create_ns
48+
from utilities.operator_utils import get_cluster_service_version
4149
from utilities.resources.model_registry_modelregistry_opendatahub_io import ModelRegistry
4250

4351
LOGGER = get_logger(name=__name__)
@@ -408,3 +416,326 @@ def model_registry_default_postgres_deployment_match_label(
408416
ensure_exists=True,
409417
)
410418
return deployment.instance.spec.selector.matchLabels
419+
420+
421+
# Model Registry Deployment Fixtures (similar to HuggingFace deployment fixtures)
422+
423+
424+
class ModelRegistryDeploymentError(Exception):
425+
"""Exception raised when model registry deployment fails."""
426+
427+
428+
class PredictorPodNotFoundError(Exception):
429+
"""Exception raised when predictor pods are not found for an InferenceService."""
430+
431+
432+
def get_openvino_image_from_rhoai_csv(admin_client: DynamicClient) -> str:
433+
"""
434+
Get the OpenVINO model server image from the RHOAI ClusterServiceVersion.
435+
436+
Returns:
437+
str: The OpenVINO model server image URL from RHOAI CSV
438+
439+
Raises:
440+
Exception: If unable to find the image in the CSV
441+
"""
442+
# Get the RHOAI CSV using the utility function
443+
csv = get_cluster_service_version(
444+
client=admin_client, prefix="rhods-operator", namespace=py_config["applications_namespace"]
445+
)
446+
447+
# Look for OpenVINO image in spec.relatedImages
448+
related_images = csv.instance.spec.get("relatedImages", [])
449+
450+
for image_info in related_images:
451+
image_url = image_info.get("image", "")
452+
if "odh-openvino-model-server" in image_url:
453+
LOGGER.info(f"Found OpenVINO image from RHOAI CSV: {image_url}")
454+
return image_url
455+
456+
raise ModelRegistryDeploymentError("Could not find odh-openvino-model-server image in RHOAI CSV relatedImages")
457+
458+
459+
@pytest.fixture(scope="class")
460+
def model_registry_deployment_ns(admin_client: DynamicClient) -> Generator[Namespace, Any, Any]:
461+
"""
462+
Create a dedicated namespace for Model Registry model deployments and testing.
463+
Similar to hugging_face_deployment_ns but specifically for registered model serving tests.
464+
"""
465+
with create_ns(
466+
name="mr-deployment-ns",
467+
admin_client=admin_client,
468+
) as ns:
469+
LOGGER.info(f"Created Model Registry deployment namespace: {ns.name}")
470+
yield ns
471+
472+
473+
@pytest.fixture(scope="class")
474+
def model_registry_connection_secret(
475+
admin_client: DynamicClient,
476+
model_registry_deployment_ns: Namespace,
477+
registered_model_rest_api: dict[str, Any],
478+
) -> Generator[Secret, Any, Any]:
479+
"""
480+
Create a connection secret for the registered model URI.
481+
This secret is required by the ODH admission webhook when creating InferenceServices
482+
with the opendatahub.io/connections annotation.
483+
"""
484+
resource_name = "mr-test-inference-service-connection"
485+
# Use the model URI from the registered model
486+
register_model_data = registered_model_rest_api.get("register_model", {})
487+
model_uri = register_model_data.get("external_id", "hf://jonburdo/test2")
488+
489+
# Base64 encode the model URI
490+
encoded_uri = base64.b64encode(model_uri.encode()).decode()
491+
492+
# Annotations matching the connection secret structure
493+
annotations = {
494+
"opendatahub.io/connection-type-protocol": "uri",
495+
"opendatahub.io/connection-type-ref": "uri-v1",
496+
"openshift.io/display-name": resource_name,
497+
}
498+
499+
# Labels for ODH integration
500+
labels = {
501+
"opendatahub.io/dashboard": "false",
502+
}
503+
504+
with Secret(
505+
client=admin_client,
506+
name=resource_name,
507+
namespace=model_registry_deployment_ns.name,
508+
annotations=annotations,
509+
label=labels,
510+
data_dict={"URI": encoded_uri},
511+
teardown=True,
512+
) as connection_secret:
513+
LOGGER.info(
514+
f"Created Model Registry connection secret: {resource_name} in "
515+
f"namespace: {model_registry_deployment_ns.name}"
516+
)
517+
yield connection_secret
518+
519+
520+
@pytest.fixture(scope="class")
521+
def model_registry_serving_runtime(
522+
admin_client: DynamicClient,
523+
model_registry_deployment_ns: Namespace,
524+
) -> Generator[ServingRuntime, Any, Any]:
525+
"""
526+
Create a ServingRuntime for OpenVINO Model Server to support registered models.
527+
Based on the HuggingFace serving runtime with complete ODH dashboard integration.
528+
"""
529+
runtime_name = "mr-test-runtime"
530+
531+
# Complete annotations matching manually created examples
532+
annotations = {
533+
"opendatahub.io/apiProtocol": "REST",
534+
"opendatahub.io/recommended-accelerators": '["nvidia.com/gpu"]',
535+
"opendatahub.io/runtime-version": "v2025.4",
536+
"opendatahub.io/serving-runtime-scope": "global",
537+
"opendatahub.io/template-display-name": "OpenVINO Model Server",
538+
"opendatahub.io/template-name": "kserve-ovms",
539+
"openshift.io/display-name": "OpenVINO Model Server",
540+
}
541+
542+
# Labels for ODH dashboard integration
543+
labels = {
544+
"opendatahub.io/dashboard": "true",
545+
}
546+
547+
# Supported model formats
548+
supported_model_formats = [
549+
{"autoSelect": True, "name": "openvino_ir", "version": "opset13"},
550+
{"name": "onnx", "version": "1"},
551+
{"autoSelect": True, "name": "tensorflow", "version": "1"},
552+
{"autoSelect": True, "name": "tensorflow", "version": "2"},
553+
{"autoSelect": True, "name": "paddle", "version": "2"},
554+
{"autoSelect": True, "name": "pytorch", "version": "2"},
555+
]
556+
557+
# Complete ServingRuntime specification
558+
runtime_spec = {
559+
"annotations": {
560+
"opendatahub.io/kserve-runtime": "ovms",
561+
"prometheus.io/path": "/metrics",
562+
"prometheus.io/port": "8888",
563+
},
564+
"containers": [
565+
{
566+
"args": [
567+
"--model_name={{.Name}}",
568+
"--port=8001",
569+
"--rest_port=8888",
570+
"--model_path=/mnt/models",
571+
"--file_system_poll_wait_seconds=0",
572+
"--metrics_enable",
573+
],
574+
"image": get_openvino_image_from_rhoai_csv(admin_client),
575+
"name": "kserve-container",
576+
"ports": [{"containerPort": 8888, "protocol": "TCP"}],
577+
}
578+
],
579+
"multiModel": False,
580+
"protocolVersions": ["v2", "grpc-v2"],
581+
"supportedModelFormats": supported_model_formats,
582+
}
583+
584+
# Create the ServingRuntime with complete configuration
585+
runtime_dict = {
586+
"apiVersion": "serving.kserve.io/v1alpha1",
587+
"kind": "ServingRuntime",
588+
"metadata": {
589+
"name": runtime_name,
590+
"namespace": model_registry_deployment_ns.name,
591+
"annotations": annotations,
592+
"labels": labels,
593+
},
594+
"spec": runtime_spec,
595+
}
596+
597+
with ServingRuntime(
598+
client=admin_client,
599+
kind_dict=runtime_dict,
600+
teardown=True,
601+
) as serving_runtime:
602+
LOGGER.info(
603+
f"Created OpenVINO ServingRuntime: {runtime_name} in namespace: {model_registry_deployment_ns.name}"
604+
)
605+
yield serving_runtime
606+
607+
608+
@pytest.fixture(scope="class")
609+
def model_registry_inference_service(
610+
admin_client: DynamicClient,
611+
model_registry_deployment_ns: Namespace,
612+
model_registry_serving_runtime: ServingRuntime,
613+
model_registry_connection_secret: Secret,
614+
registered_model_rest_api: dict[str, Any],
615+
) -> Generator[InferenceService, Any, Any]:
616+
"""
617+
Create an InferenceService for testing registered models.
618+
Based on the HuggingFace InferenceService with comprehensive ODH dashboard integration.
619+
"""
620+
name = "mr-test-inference-service"
621+
# Use the model URI from the registered model
622+
register_model_data = registered_model_rest_api.get("register_model", {})
623+
model_uri = register_model_data.get("external_id", "hf://jonburdo/test2")
624+
model_name = register_model_data.get("name", "my-model")
625+
runtime_name = model_registry_serving_runtime.name
626+
627+
# Resources
628+
resources = {"limits": {"cpu": "2", "memory": "4Gi"}, "requests": {"cpu": "2", "memory": "4Gi"}}
629+
630+
# Labels for ODH dashboard integration
631+
labels = {
632+
"opendatahub.io/dashboard": "true",
633+
}
634+
635+
# Comprehensive annotations matching ODH integration
636+
annotations = {
637+
"opendatahub.io/connections": model_registry_connection_secret.name,
638+
"opendatahub.io/hardware-profile-name": "default-profile",
639+
"opendatahub.io/hardware-profile-namespace": "redhat-ods-applications",
640+
"opendatahub.io/model-type": "predictive",
641+
"openshift.io/description": f"Model from registry: {model_name}",
642+
"openshift.io/display-name": f"registry/{name}",
643+
"security.opendatahub.io/enable-auth": "false",
644+
"serving.kserve.io/deploymentMode": "RawDeployment",
645+
}
646+
647+
# Predictor configuration
648+
predictor_dict = {
649+
"automountServiceAccountToken": False,
650+
"deploymentStrategy": {"type": "RollingUpdate"},
651+
"maxReplicas": 1,
652+
"minReplicas": 1,
653+
"model": {
654+
"modelFormat": {"name": "onnx", "version": "1"},
655+
"name": "",
656+
"resources": resources,
657+
"runtime": runtime_name,
658+
"storageUri": model_uri,
659+
},
660+
}
661+
662+
with InferenceService(
663+
client=admin_client,
664+
name=name,
665+
namespace=model_registry_deployment_ns.name,
666+
annotations=annotations,
667+
label=labels,
668+
predictor=predictor_dict,
669+
teardown=True,
670+
) as inference_service:
671+
# Wait for InferenceService to become Ready
672+
inference_service.wait_for_condition(
673+
condition="Ready",
674+
status="True",
675+
timeout=600, # 10 minutes timeout for model loading
676+
)
677+
LOGGER.info(
678+
f"Created Model Registry InferenceService: {name} in namespace: {model_registry_deployment_ns.name}"
679+
)
680+
yield inference_service
681+
682+
683+
@pytest.fixture(scope="class")
684+
def model_registry_predictor_pod(
685+
admin_client: DynamicClient,
686+
model_registry_deployment_ns: Namespace,
687+
model_registry_inference_service: InferenceService,
688+
) -> Pod:
689+
"""
690+
Get the predictor pod for the Model Registry InferenceService.
691+
"""
692+
namespace = model_registry_deployment_ns.name
693+
label_selector = f"serving.kserve.io/inferenceservice={model_registry_inference_service.name}"
694+
695+
pods = Pod.get(
696+
client=admin_client,
697+
namespace=namespace,
698+
label_selector=label_selector,
699+
)
700+
701+
predictor_pods = [pod for pod in pods if "predictor" in pod.name]
702+
if not predictor_pods:
703+
raise PredictorPodNotFoundError(
704+
f"No predictor pods found for InferenceService {model_registry_inference_service.name}"
705+
)
706+
707+
pod = predictor_pods[0] # Use the first predictor pod
708+
LOGGER.info(f"Found predictor pod: {pod.name} in namespace: {namespace}")
709+
return pod
710+
711+
712+
@pytest.fixture(scope="class")
713+
def model_registry_model_portforward(
714+
model_registry_deployment_ns: Namespace,
715+
model_registry_inference_service: InferenceService,
716+
model_registry_predictor_pod: Pod,
717+
) -> Generator[str, Any]:
718+
"""
719+
Port-forwards the Model Registry OpenVINO model server pod to access the model API locally.
720+
Equivalent CLI:
721+
oc -n mr-deployment-ns port-forward pod/<pod-name> 8080:8888
722+
"""
723+
namespace = model_registry_deployment_ns.name
724+
local_port = 9998 # Different from HF to avoid conflicts
725+
remote_port = 8888 # OpenVINO Model Server REST port
726+
local_url = f"http://localhost:{local_port}/v1/models"
727+
728+
try:
729+
with portforward.forward(
730+
pod_or_service=model_registry_predictor_pod.name,
731+
namespace=namespace,
732+
from_port=local_port,
733+
to_port=remote_port,
734+
waiting=20,
735+
):
736+
LOGGER.info(f"Model Registry model port-forward established: {local_url}")
737+
LOGGER.info(f"Test with: curl -s {local_url}/{model_registry_inference_service.name}")
738+
yield local_url
739+
except Exception as expt:
740+
LOGGER.error(f"Failed to set up port forwarding for pod {model_registry_predictor_pod.name}: {expt}")
741+
raise

tests/model_registry/model_registry/rest_api/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
MODEL_ARTIFACT: dict[str, Any] = {
2626
"name": "model-artifact-rest-api",
2727
"description": "Model artifact created via rest call",
28-
"uri": "https://huggingface.co/openai-community/gpt2/resolve/main/onnx/decoder_model.onnx",
28+
"uri": "hf://jonburdo/test2",
2929
"state": "UNKNOWN",
3030
"modelFormatName": ModelFormat.ONNX,
3131
"modelFormatVersion": "v1",

0 commit comments

Comments
 (0)