Skip to content

Commit c3d0741

Browse files
RHOAIENG-24934 - Test infrastructure for RAG (opendatahub-io#335)
* RHOAIENG-24934 - Test infrastructure for RAG * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2cba7b0 commit c3d0741

9 files changed

Lines changed: 199 additions & 31 deletions

File tree

tests/model_registry/rbac/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from pyhelper_utils.shell import run_command
1919

2020
from tests.model_registry.rbac.utils import wait_for_oauth_openshift_deployment, create_role_binding
21-
from tests.model_registry.utils import generate_random_name, generate_namespace_name
21+
from utilities.general import generate_random_name
22+
from tests.model_registry.utils import generate_namespace_name
2223
from utilities.infra import login_with_user_password
2324
from utilities.user_utils import UserTestSession, create_htpasswd_file, wait_for_user_creation
2425
from tests.model_registry.rbac.group_utils import create_group

tests/model_registry/rest_api/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
execute_model_registry_patch_command,
1010
)
1111
from utilities.constants import Protocols
12+
from utilities.general import generate_random_name
1213
from ocp_resources.deployment import Deployment
1314
from tests.model_registry.utils import (
1415
get_model_registry_deployment_template_dict,
1516
apply_mysql_args_and_volume_mounts,
1617
add_mysql_certs_volumes_to_deployment,
17-
generate_random_name,
1818
)
1919

2020
from tests.model_registry.constants import (

tests/model_registry/utils.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import uuid
21
from typing import Any, List
32

43
from kubernetes.dynamic import DynamicClient
@@ -233,34 +232,6 @@ def wait_for_pods_running(
233232
return None
234233

235234

236-
def generate_random_name(prefix: str = "", length: int = 8) -> str:
237-
"""
238-
Generates a name with a required prefix and a random suffix derived from a UUID.
239-
240-
The length of the random suffix can be controlled, defaulting to 8 characters.
241-
The suffix is taken from the beginning of a V4 UUID's hex representation.
242-
243-
Args:
244-
prefix (str): The required prefix for the generated name.
245-
length (int, optional): The desired length for the UUID-derived suffix.
246-
Defaults to 8. Must be between 1 and 32.
247-
248-
Returns:
249-
str: A string in the format "prefix-uuid_suffix".
250-
251-
Raises:
252-
ValueError: If prefix is empty, or if length is not between 1 and 32.
253-
"""
254-
if not isinstance(length, int) or not (1 <= length <= 32):
255-
raise ValueError("suffix_length must be an integer between 1 and 32.")
256-
# Generate a new random UUID (version 4)
257-
random_uuid = uuid.uuid4()
258-
# Use the first 'length' characters of the hexadecimal representation of the UUID as the suffix.
259-
# random_uuid.hex is 32 characters long.
260-
suffix = random_uuid.hex[:length]
261-
return f"{prefix}-{suffix}" if prefix else suffix
262-
263-
264235
def generate_namespace_name(file_path: str) -> str:
265236
return (file_path.removesuffix(".py").replace("/", "-").replace("_", "-"))[-63:].split("-", 1)[-1]
266237

tests/rag/__init__.py

Whitespace-only changes.

tests/rag/conftest.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from typing import Dict, Generator, Any
2+
import pytest
3+
import os
4+
from kubernetes.dynamic import DynamicClient
5+
from ocp_resources.data_science_cluster import DataScienceCluster
6+
from ocp_resources.deployment import Deployment
7+
from _pytest.fixtures import FixtureRequest
8+
from ocp_resources.namespace import Namespace
9+
from utilities.infra import create_ns
10+
from simple_logger.logger import get_logger
11+
from utilities.rag_utils import create_llama_stack_distribution, LlamaStackDistribution
12+
from utilities.data_science_cluster_utils import update_components_in_dsc
13+
from utilities.constants import DscComponents, Timeout
14+
from utilities.general import generate_random_name
15+
from timeout_sampler import TimeoutSampler
16+
17+
LOGGER = get_logger(name=__name__)
18+
19+
20+
def llama_stack_server() -> Dict[str, Any]:
21+
rag_vllm_url = os.getenv("RAG_VLLM_URL")
22+
rag_vllm_model = os.getenv("RAG_VLLM_MODEL")
23+
rag_vllm_token = os.getenv("RAG_VLLM_TOKEN")
24+
25+
return {
26+
"containerSpec": {
27+
"env": [
28+
{"name": "INFERENCE_MODEL", "value": rag_vllm_model},
29+
{"name": "VLLM_TLS_VERIFY", "value": "false"},
30+
{"name": "VLLM_API_TOKEN", "value": rag_vllm_token},
31+
{"name": "VLLM_URL", "value": rag_vllm_url},
32+
{"name": "MILVUS_DB_PATH", "value": "/.llama/distributions/remote-vllm/milvus.db"},
33+
],
34+
"name": "llama-stack",
35+
"port": 8321,
36+
},
37+
"distribution": {"image": "quay.io/mcampbel/llama-stack:milvus-granite-embedding-125m-english"},
38+
"podOverrides": {
39+
"volumeMounts": [{"mountPath": "/root/.llama", "name": "llama-storage"}],
40+
"volumes": [{"emptyDir": {}, "name": "llama-storage"}],
41+
},
42+
}
43+
44+
45+
@pytest.fixture(scope="class")
46+
def enabled_llama_stack_operator(dsc_resource: DataScienceCluster) -> Generator[None, Any, Any]:
47+
with update_components_in_dsc(
48+
dsc=dsc_resource,
49+
components={
50+
DscComponents.LLAMASTACKOPERATOR: DscComponents.ManagementState.MANAGED,
51+
},
52+
wait_for_components_state=True,
53+
) as dsc:
54+
yield dsc
55+
56+
57+
@pytest.fixture(scope="function")
58+
def rag_test_namespace(unprivileged_client: DynamicClient) -> Generator[Namespace, Any, Any]:
59+
namespace_name = generate_random_name(prefix="rag-test-")
60+
with create_ns(namespace_name, unprivileged_client=unprivileged_client) as ns:
61+
yield ns
62+
63+
64+
@pytest.fixture(scope="function")
65+
def llama_stack_distribution_from_template(
66+
enabled_llama_stack_operator: Generator[None, Any, Any],
67+
rag_test_namespace: Namespace,
68+
request: FixtureRequest,
69+
admin_client: DynamicClient,
70+
) -> Generator[LlamaStackDistribution, Any, Any]:
71+
with create_llama_stack_distribution(
72+
client=admin_client,
73+
name="rag-llama-stack-distribution",
74+
namespace=rag_test_namespace.name,
75+
replicas=1,
76+
server=llama_stack_server(),
77+
) as llama_stack_distribution:
78+
yield llama_stack_distribution
79+
80+
81+
@pytest.fixture(scope="function")
82+
def llama_stack_distribution_deployment(
83+
rag_test_namespace: Namespace,
84+
admin_client: DynamicClient,
85+
llama_stack_distribution_from_template: Generator[LlamaStackDistribution, Any, Any],
86+
) -> Generator[Deployment, Any, Any]:
87+
deployment = Deployment(
88+
client=admin_client,
89+
namespace=rag_test_namespace.name,
90+
name="rag-llama-stack-distribution",
91+
)
92+
93+
timeout = Timeout.TIMEOUT_15_SEC
94+
sampler = TimeoutSampler(
95+
wait_timeout=timeout, sleep=1, func=lambda deployment: deployment.exists is not None, deployment=deployment
96+
)
97+
for item in sampler:
98+
if item:
99+
break # Break after first successful iteration
100+
101+
assert deployment.exists, f"llama stack distribution deployment doesn't exist within {timeout} seconds"
102+
yield deployment

tests/rag/test_rag.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class TestRag:
2+
def test_rag_deployment(self, llama_stack_distribution_deployment):
3+
"""
4+
Test that the Llama stack distribution deployment for
5+
RAG was created and it has a working pod.
6+
7+
This verifies that:
8+
1. The Llama stack operator is up.
9+
2. It is possible to create a Llama stack distribution.
10+
3. A pod for the Llama stack distribution starts correctly.
11+
"""
12+
llama_stack_distribution_deployment.wait_for_replicas()

utilities/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class DscComponents:
153153
MODELMESHSERVING: str = "modelmeshserving"
154154
KSERVE: str = "kserve"
155155
MODELREGISTRY: str = "modelregistry"
156+
LLAMASTACKOPERATOR: str = "llamastackoperator"
156157

157158
class ManagementState:
158159
MANAGED: str = "Managed"
@@ -162,11 +163,13 @@ class ConditionType:
162163
MODEL_REGISTRY_READY: str = "ModelRegistryReady"
163164
KSERVE_READY: str = "KserveReady"
164165
MODEL_MESH_SERVING_READY: str = "ModelMeshServingReady"
166+
LLAMA_STACK_OPERATOR_READY: str = "LlamaStackOperatorReady"
165167

166168
COMPONENT_MAPPING: dict[str, str] = {
167169
MODELMESHSERVING: ConditionType.MODEL_MESH_SERVING_READY,
168170
KSERVE: ConditionType.KSERVE_READY,
169171
MODELREGISTRY: ConditionType.MODEL_REGISTRY_READY,
172+
LLAMASTACKOPERATOR: ConditionType.LLAMA_STACK_OPERATOR_READY,
170173
}
171174

172175

utilities/general.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import re
33
from typing import List, Tuple
4+
import uuid
45

56
from kubernetes.dynamic import DynamicClient
67
from kubernetes.dynamic.exceptions import ResourceNotFoundError
@@ -302,3 +303,31 @@ def create_ig_pod_label_selector_str(ig: InferenceGraph) -> str:
302303
303304
"""
304305
return f"serving.kserve.io/inferencegraph={ig.name}"
306+
307+
308+
def generate_random_name(prefix: str = "", length: int = 8) -> str:
309+
"""
310+
Generates a name with a required prefix and a random suffix derived from a UUID.
311+
312+
The length of the random suffix can be controlled, defaulting to 8 characters.
313+
The suffix is taken from the beginning of a V4 UUID's hex representation.
314+
315+
Args:
316+
prefix (str): The required prefix for the generated name.
317+
length (int, optional): The desired length for the UUID-derived suffix.
318+
Defaults to 8. Must be between 1 and 32.
319+
320+
Returns:
321+
str: A string in the format "prefix-uuid_suffix".
322+
323+
Raises:
324+
ValueError: If prefix is empty, or if length is not between 1 and 32.
325+
"""
326+
if not isinstance(length, int) or not (1 <= length <= 32):
327+
raise ValueError("suffix_length must be an integer between 1 and 32.")
328+
# Generate a new random UUID (version 4)
329+
random_uuid = uuid.uuid4()
330+
# Use the first 'length' characters of the hexadecimal representation of the UUID as the suffix.
331+
# random_uuid.hex is 32 characters long.
332+
suffix = random_uuid.hex[:length]
333+
return f"{prefix}-{suffix}" if prefix else suffix

utilities/rag_utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from contextlib import contextmanager
2+
from ocp_resources.resource import NamespacedResource
3+
from kubernetes.dynamic import DynamicClient
4+
from typing import Any, Dict, Generator
5+
6+
7+
class LlamaStackDistribution(NamespacedResource):
8+
api_group: str = "llamastack.io"
9+
10+
def __init__(self, replicas: int, server: Dict[str, Any], **kwargs: Any):
11+
"""
12+
Args:
13+
kwargs: Keyword arguments to pass to the LlamaStackDistribution constructor
14+
"""
15+
super().__init__(
16+
**kwargs,
17+
)
18+
self.replicas = replicas
19+
self.server = server
20+
21+
def to_dict(self) -> None:
22+
super().to_dict()
23+
if not self.kind_dict and not self.yaml_file:
24+
self.res["spec"] = {}
25+
_spec = self.res["spec"]
26+
_spec["replicas"] = self.replicas
27+
_spec["server"] = self.server
28+
29+
30+
@contextmanager
31+
def create_llama_stack_distribution(
32+
client: DynamicClient,
33+
name: str,
34+
namespace: str,
35+
replicas: int,
36+
server: Dict[str, Any],
37+
teardown: bool = True,
38+
) -> Generator[LlamaStackDistribution, Any, Any]:
39+
"""
40+
Context manager to create and optionally delete a LLama Stack Distribution
41+
"""
42+
with LlamaStackDistribution(
43+
client=client,
44+
name=name,
45+
namespace=namespace,
46+
replicas=replicas,
47+
server=server,
48+
teardown=teardown,
49+
) as llama_stack_distribution:
50+
yield llama_stack_distribution

0 commit comments

Comments
 (0)