Skip to content

Commit 6e186da

Browse files
authored
Merge branch 'main' into byoidc-unprivileged-user-login
2 parents ee1d164 + 938089c commit 6e186da

File tree

4 files changed

+540
-36
lines changed

4 files changed

+540
-36
lines changed

tests/model_registry/async_job/utils.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
31
import requests
42
from kubernetes.dynamic import DynamicClient
53
from ocp_resources.job import Job
@@ -9,7 +7,7 @@
97
from simple_logger.logger import get_logger
108
from timeout_sampler import TimeoutExpiredError
119

12-
from utilities.must_gather_collector import get_base_dir, get_must_gather_collector_dir
10+
from utilities.general import collect_pod_information
1311

1412
LOGGER = get_logger(name=__name__)
1513

@@ -136,22 +134,3 @@ def upload_test_model_to_minio_from_image(
136134
LOGGER.info(
137135
f"Test model file uploaded successfully to s3://{MinIo.Buckets.MODELMESH_EXAMPLE_MODELS}/{object_key}"
138136
)
139-
140-
141-
def collect_pod_information(pod: Pod) -> None:
142-
try:
143-
base_dir_name = get_must_gather_collector_dir() or get_base_dir()
144-
LOGGER.info(f"Collecting pod information for {pod.name}: {base_dir_name}")
145-
os.makedirs(base_dir_name, exist_ok=True)
146-
yaml_file_path = os.path.join(base_dir_name, f"{pod.name}.yaml")
147-
with open(yaml_file_path, "w") as fd:
148-
fd.write(pod.instance.to_str())
149-
# get all the containers of the pod:
150-
151-
containers = [container["name"] for container in pod.instance.status.containerStatuses]
152-
for container in containers:
153-
file_path = os.path.join(base_dir_name, f"{pod.name}_{container}.log")
154-
with open(file_path, "w") as fd:
155-
fd.write(pod.log(**{"container": container}))
156-
except Exception:
157-
LOGGER.warning(f"For pod: {pod.name} information gathering failed.")

tests/workbenches/conftest.py

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
from ocp_resources.namespace import Namespace
1212
from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
1313
from ocp_resources.notebook import Notebook
14+
from ocp_resources.pod import Pod
1415

15-
from utilities.constants import Labels
16+
from utilities.constants import Labels, Timeout
1617
from utilities import constants
1718
from utilities.constants import INTERNAL_IMAGE_REGISTRY_PATH
1819
from utilities.infra import check_internal_image_registry_available
20+
from utilities.general import collect_pod_information
1921

2022
LOGGER = get_logger(name=__name__)
2123

@@ -44,10 +46,64 @@ def minimal_image() -> Generator[str, None, None]:
4446

4547

4648
@pytest.fixture(scope="function")
47-
def default_notebook(
49+
def notebook_image(
4850
request: pytest.FixtureRequest,
4951
admin_client: DynamicClient,
5052
minimal_image: str,
53+
) -> str:
54+
"""
55+
Resolves the notebook image path.
56+
57+
Priority:
58+
1. 'custom_image' provided via indirect parametrization
59+
2. Default 'minimal_image' (with automatic registry resolution)
60+
"""
61+
# SAFELY get parameters. If test doesn't parameterize this fixture, default to empty dict.
62+
params = getattr(request, "param", {})
63+
custom_image = params.get("custom_image")
64+
65+
# Case A: Custom Image (Explicit)
66+
if custom_image:
67+
custom_image = custom_image.strip()
68+
if not custom_image:
69+
raise ValueError("custom_image cannot be empty or whitespace")
70+
71+
# Validation Logic: Only digest references are accepted
72+
_ERR_INVALID_CUSTOM_IMAGE = (
73+
"custom_image must be a valid OCI image reference with a digest (@sha256:digest), "
74+
"e.g., 'quay.io/org/image@sha256:abc123...', "
75+
"got: '{custom_image}'"
76+
)
77+
# Check for valid digest: @sha256: must be followed by non-empty content
78+
digest_marker = "@sha256:"
79+
has_valid_digest = False
80+
if digest_marker in custom_image:
81+
digest_index = custom_image.rfind(digest_marker)
82+
digest_end = digest_index + len(digest_marker)
83+
has_valid_digest = digest_end < len(custom_image)
84+
85+
if not has_valid_digest:
86+
raise ValueError(_ERR_INVALID_CUSTOM_IMAGE.format(custom_image=custom_image))
87+
88+
LOGGER.info(f"Using custom workbench image: {custom_image}")
89+
return custom_image
90+
91+
# Case B: Default Image (Implicit / Good Default)
92+
# This runs for all standard tests in test_spawning.py
93+
internal_image_registry = check_internal_image_registry_available(admin_client=admin_client)
94+
95+
return (
96+
f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{minimal_image}"
97+
if internal_image_registry
98+
else minimal_image
99+
)
100+
101+
102+
@pytest.fixture(scope="function")
103+
def default_notebook(
104+
request: pytest.FixtureRequest,
105+
admin_client: DynamicClient,
106+
notebook_image: str,
51107
) -> Generator[Notebook, None, None]:
52108
"""Returns a new Notebook CR for a given namespace, name, and image"""
53109
namespace = request.param["namespace"]
@@ -60,15 +116,8 @@ def default_notebook(
60116
username = get_username(dyn_client=admin_client)
61117
assert username, "Failed to determine username from the cluster"
62118

63-
# Check internal image registry availability
64-
internal_image_registry = check_internal_image_registry_available(admin_client=admin_client)
65-
66-
# Set the image path based on internal image registry status
67-
minimal_image_path = (
68-
f"{INTERNAL_IMAGE_REGISTRY_PATH}/{py_config['applications_namespace']}/{minimal_image}"
69-
if internal_image_registry
70-
else ":" + minimal_image.rsplit(":", maxsplit=1)[1]
71-
)
119+
# Set the image path based on the resolved notebook_image
120+
image_path = notebook_image
72121

73122
probe_config = {
74123
"failureThreshold": 3,
@@ -90,7 +139,7 @@ def default_notebook(
90139
"annotations": {
91140
Labels.Notebook.INJECT_AUTH: "true",
92141
"opendatahub.io/accelerator-name": "",
93-
"notebooks.opendatahub.io/last-image-selection": minimal_image,
142+
"notebooks.opendatahub.io/last-image-selection": image_path,
94143
# Add any additional annotations if provided
95144
**auth_annotations,
96145
},
@@ -124,9 +173,9 @@ def default_notebook(
124173
" "
125174
"--ServerApp.quit_button=False\n",
126175
},
127-
{"name": "JUPYTER_IMAGE", "value": minimal_image_path},
176+
{"name": "JUPYTER_IMAGE", "value": image_path},
128177
],
129-
"image": minimal_image_path,
178+
"image": image_path,
130179
"imagePullPolicy": "Always",
131180
"livenessProbe": probe_config,
132181
"name": name,
@@ -167,3 +216,70 @@ def default_notebook(
167216

168217
with Notebook(kind_dict=notebook) as nb:
169218
yield nb
219+
220+
221+
@pytest.fixture(scope="function")
222+
def notebook_pod(
223+
unprivileged_client: DynamicClient,
224+
default_notebook: Notebook,
225+
) -> Pod:
226+
"""
227+
Returns a notebook pod in Ready state.
228+
229+
This fixture:
230+
- Creates a Pod object for the notebook
231+
- Waits for pod to exist
232+
- Waits for pod to reach Ready state (10-minute timeout)
233+
- Provides detailed diagnostics on failure
234+
235+
Args:
236+
unprivileged_client: Client for interacting with the cluster
237+
default_notebook: The notebook CR to get the pod for
238+
239+
Returns:
240+
Pod object in Ready state
241+
242+
Raises:
243+
AssertionError: If pod fails to reach Ready state or is not created
244+
"""
245+
# Error messages
246+
_ERR_POD_NOT_READY = (
247+
"Pod '{pod_name}-0' failed to reach Ready state within 10 minutes.\n"
248+
"Pod Phase: {pod_phase}\n"
249+
"Original Error: {original_error}\n"
250+
"Pod information collected to must-gather directory for debugging."
251+
)
252+
_ERR_POD_NOT_CREATED = "Pod '{pod_name}-0' was not created. Check notebook controller logs."
253+
254+
# Create pod object
255+
notebook_pod = Pod(
256+
client=unprivileged_client,
257+
namespace=default_notebook.namespace,
258+
name=f"{default_notebook.name}-0",
259+
)
260+
261+
try:
262+
notebook_pod.wait()
263+
notebook_pod.wait_for_condition(
264+
condition=Pod.Condition.READY,
265+
status=Pod.Condition.Status.TRUE,
266+
timeout=Timeout.TIMEOUT_10MIN,
267+
)
268+
except (TimeoutError, RuntimeError) as e:
269+
if notebook_pod.exists:
270+
# Collect pod information for debugging purposes (YAML + logs saved to must-gather dir)
271+
collect_pod_information(notebook_pod)
272+
pod_status = notebook_pod.instance.status
273+
pod_phase = pod_status.phase
274+
raise AssertionError(
275+
_ERR_POD_NOT_READY.format(
276+
pod_name=default_notebook.name,
277+
pod_phase=pod_phase,
278+
original_error=e,
279+
)
280+
) from e
281+
else:
282+
# Pod was never created
283+
raise AssertionError(_ERR_POD_NOT_CREATED.format(pod_name=default_notebook.name)) from e
284+
285+
return notebook_pod

0 commit comments

Comments
 (0)