Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
501 changes: 128 additions & 373 deletions tests/model_registry/conftest.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import shlex
from simple_logger.logger import get_logger

import pytest
from ocp_utilities.monitoring import Prometheus
from pyhelper_utils.shell import run_command

LOGGER = get_logger(name=__name__)


def get_prometheus_k8s_token(duration: str = "1800s") -> str:
token_command = f"oc create token prometheus-k8s -n openshift-monitoring --duration={duration}"
Expand All @@ -21,16 +17,3 @@ def prometheus_for_monitoring() -> Prometheus:
verify_ssl=False,
bearer_token=get_prometheus_k8s_token(duration="86400s"),
)


@pytest.mark.order("last")
def test_mr_operator_not_oomkilled(prometheus_for_monitoring: Prometheus):
result = prometheus_for_monitoring.query_sampler(
query='kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}'
)
if result:
for entry in result:
LOGGER.info(entry)
pod_name = entry["metric"]["pod"]
if pod_name.startswith("model-registry-operator-controller-manager"):
pytest.fail(f"Pod {pod_name} was oomkilled: {entry}")
19 changes: 19 additions & 0 deletions tests/model_registry/health/test_mr_operator_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from simple_logger.logger import get_logger

import pytest
from ocp_utilities.monitoring import Prometheus

LOGGER = get_logger(name=__name__)


@pytest.mark.order("last")
def test_mr_operator_not_oomkilled(prometheus_for_monitoring: Prometheus):
result = prometheus_for_monitoring.query_sampler(
query='kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}'
)
if result:
for entry in result:
LOGGER.info(entry)
pod_name = entry["metric"]["pod"]
if pod_name.startswith("model-registry-operator-controller-manager"):
pytest.fail(f"Pod {pod_name} was oomkilled: {entry}")
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from tests.model_registry.constants import DEFAULT_CUSTOM_MODEL_CATALOG, DEFAULT_MODEL_CATALOG_CM
from tests.model_registry.model_catalog.constants import REDHAT_AI_CATALOG_ID, CATALOG_CONTAINER, DEFAULT_CATALOGS
from tests.model_registry.model_catalog.utils import (
from tests.model_registry.model_catalog.catalog_config.utils import (
validate_model_catalog_enabled,
validate_model_catalog_resource,
get_validate_default_model_catalog_source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ocp_resources.resource import ResourceEditor
from tests.model_registry.constants import DEFAULT_MODEL_CATALOG_CM
from tests.model_registry.model_catalog.constants import DEFAULT_CATALOGS
from tests.model_registry.model_catalog.utils import validate_model_catalog_configmap_data
from tests.model_registry.model_catalog.catalog_config.utils import validate_model_catalog_configmap_data
from tests.model_registry.utils import execute_get_command

LOGGER = get_logger(name=__name__)
Expand Down
114 changes: 114 additions & 0 deletions tests/model_registry/model_catalog/catalog_config/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import Any
import yaml

from kubernetes.dynamic import DynamicClient
from simple_logger.logger import get_logger

from ocp_resources.pod import Pod
from ocp_resources.config_map import ConfigMap
from tests.model_registry.model_catalog.constants import DEFAULT_CATALOGS

LOGGER = get_logger(name=__name__)


def validate_model_catalog_enabled(pod: Pod) -> bool:
for container in pod.instance.spec.containers:
for env in container.env:
if env.name == "ENABLE_MODEL_CATALOG":
return True
return False


def validate_model_catalog_resource(
kind: Any, admin_client: DynamicClient, namespace: str, expected_resource_count: int
) -> None:
resource = list(kind.get(namespace=namespace, label_selector="component=model-catalog", dyn_client=admin_client))
assert resource
LOGGER.info(f"Validating resource: {kind}: Found {len(resource)}")
assert len(resource) == expected_resource_count, (
f"Unexpected number of {kind} resources found: {[res.name for res in resource]}"
)
Comment thread
fege marked this conversation as resolved.


def validate_default_catalog(catalogs: list[dict[Any, Any]]) -> None:
errors = []
for catalog in catalogs:
expected_catalog = DEFAULT_CATALOGS.get(catalog["id"])
assert expected_catalog, f"Unexpected catalog: {catalog}"
for key, expected_value in expected_catalog.items():
actual_value = catalog.get(key)
if actual_value != expected_value:
errors.append(f"For catalog '{catalog['id']}': expected {key}={expected_value}, but got {actual_value}")

assert not errors, "\n".join(errors)
Comment thread
fege marked this conversation as resolved.


def get_validate_default_model_catalog_source(catalogs: list[dict[Any, Any]]) -> None:
assert len(catalogs) == 2, f"Expected no custom models to be present. Actual: {catalogs}"
ids_actual = [entry["id"] for entry in catalogs]
assert sorted(ids_actual) == sorted(DEFAULT_CATALOGS.keys()), (
f"Actual default catalog entries: {ids_actual},Expected: {DEFAULT_CATALOGS.keys()}"
)


def extract_schema_fields(openapi_schema: dict[Any, Any], schema_name: str) -> tuple[set[str], set[str]]:
"""
Extract all and required fields from an OpenAPI schema for validation.

Args:
openapi_schema: The parsed OpenAPI schema dictionary
schema_name: Name of the schema to extract (e.g., "CatalogModel", "CatalogModelArtifact")

Returns:
Tuple of (all_fields, required_fields) excluding server-generated fields and timestamps.
"""

def _extract_properties_and_required(schema: dict[Any, Any]) -> tuple[set[str], set[str]]:
"""Recursively extract properties and required fields from a schema."""
props = set(schema.get("properties", {}).keys())
required = set(schema.get("required", []))

# Properties from allOf (inheritance/composition)
if "allOf" in schema:
for item in schema["allOf"]:
sub_schema = item
if "$ref" in item:
# Follow reference and recursively extract
ref_schema_name = item["$ref"].split("/")[-1]
sub_schema = openapi_schema["components"]["schemas"][ref_schema_name]
sub_props, sub_required = _extract_properties_and_required(schema=sub_schema)
props.update(sub_props)
required.update(sub_required)

return props, required

target_schema = openapi_schema["components"]["schemas"][schema_name]
all_properties, required_fields = _extract_properties_and_required(schema=target_schema)

# Exclude fields that shouldn't be compared
excluded_fields = {
"id", # Server-generated
"externalId", # Server-generated
"createTimeSinceEpoch", # Timestamps may differ
"lastUpdateTimeSinceEpoch", # Timestamps may differ
"artifacts", # CatalogModel only
"source_id", # CatalogModel only
}

return all_properties - excluded_fields, required_fields - excluded_fields


def validate_model_catalog_configmap_data(configmap: ConfigMap, num_catalogs: int) -> None:
"""
Validate the model catalog configmap data.

Args:
configmap: The ConfigMap object to validate
num_catalogs: Expected number of catalogs in the configmap
"""
# Check that model catalog configmaps is created when model registry is
# enabled on data science cluster.
catalogs = yaml.safe_load(configmap.instance.data["sources.yaml"])["catalogs"]
assert len(catalogs) == num_catalogs, f"{configmap.name} should have {num_catalogs} catalog"
if num_catalogs:
validate_default_catalog(catalogs=catalogs)
36 changes: 35 additions & 1 deletion tests/model_registry/model_catalog/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from ocp_resources.config_map import ConfigMap
from ocp_resources.resource import ResourceEditor
from ocp_resources.route import Route
from ocp_resources.service_account import ServiceAccount
from tests.model_registry.model_catalog.constants import (
SAMPLE_MODEL_NAME3,
Expand Down Expand Up @@ -332,7 +333,12 @@ def models_from_filter_query(


@pytest.fixture()
def labels_configmap_patch(admin_client: DynamicClient, model_registry_namespace: str) -> dict[str, Any]:
def labels_configmap_patch(
admin_client: DynamicClient,
model_registry_namespace: str,
model_catalog_rest_url: list[str],
model_registry_rest_headers: dict[str, str],
) -> Generator[dict[str, Any], None, None]:
# Get the editable ConfigMap
sources_cm = ConfigMap(name=DEFAULT_CUSTOM_MODEL_CATALOG, client=admin_client, namespace=model_registry_namespace)

Expand All @@ -352,7 +358,10 @@ def labels_configmap_patch(admin_client: DynamicClient, model_registry_namespace
patches = {"data": {"sources.yaml": yaml.dump(current_data, default_flow_style=False)}}

with ResourceEditor(patches={sources_cm: patches}):
is_model_catalog_ready(client=admin_client, model_registry_namespace=model_registry_namespace)
wait_for_model_catalog_api(url=model_catalog_rest_url[0], headers=model_registry_rest_headers)
yield patches
is_model_catalog_ready(client=admin_client, model_registry_namespace=model_registry_namespace)


@pytest.fixture()
Expand All @@ -377,3 +386,28 @@ def updated_catalog_config_map_scope_function(
wait_for_model_catalog_api(url=model_catalog_rest_url[0], headers=model_registry_rest_headers)
yield catalog_config_map
is_model_catalog_ready(client=admin_client, model_registry_namespace=model_registry_namespace)


@pytest.fixture(scope="class")
def catalog_config_map(admin_client: DynamicClient, model_registry_namespace: str) -> ConfigMap:
return ConfigMap(name=DEFAULT_CUSTOM_MODEL_CATALOG, client=admin_client, namespace=model_registry_namespace)
Comment thread
fege marked this conversation as resolved.


@pytest.fixture(scope="class")
def model_catalog_routes(admin_client: DynamicClient, model_registry_namespace: str) -> list[Route]:
return list(
Route.get(namespace=model_registry_namespace, label_selector="component=model-catalog", dyn_client=admin_client)
)


@pytest.fixture(scope="class")
def model_catalog_rest_url(model_registry_namespace: str, model_catalog_routes: list[Route]) -> list[str]:
assert model_catalog_routes, f"Model catalog routes does not exist in {model_registry_namespace}"
route_urls = [
f"https://{route.instance.spec.host}:443/api/model_catalog/v1alpha1/" for route in model_catalog_routes
]
assert route_urls, (
"Model catalog routes information could not be found from "
f"routes:{[route.name for route in model_catalog_routes]}"
)
return route_urls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import requests
import yaml
from simple_logger.logger import get_logger
from tests.model_registry.model_catalog.utils import (
from tests.model_registry.model_catalog.metadata.utils import (
execute_model_catalog_post_command,
build_catalog_preview_config,
validate_catalog_preview_counts,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from simple_logger.logger import get_logger

from tests.model_registry.model_catalog.constants import VALIDATED_CATALOG_ID
from tests.model_registry.model_catalog.utils import (
from tests.model_registry.model_catalog.metadata.utils import (
extract_custom_property_values,
validate_custom_properties_structure,
validate_custom_properties_match_metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from typing import Self
from simple_logger.logger import get_logger
from ocp_resources.config_map import ConfigMap
from tests.model_registry.model_catalog.utils import (
from tests.model_registry.model_catalog.metadata.utils import (
validate_filter_options_structure,
compare_filter_options_with_database,
)
from tests.model_registry.model_catalog.utils import (
execute_database_query,
parse_psql_output,
compare_filter_options_with_database,
)
from tests.model_registry.model_catalog.db_constants import (
FILTER_OPTIONS_DB_QUERY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from utilities.infra import get_openshift_token
from timeout_sampler import TimeoutSampler

from tests.model_registry.model_catalog.utils import (
from tests.model_registry.model_catalog.metadata.utils import (
get_labels_from_configmaps,
get_labels_from_api,
verify_labels_match,
Expand Down
Loading