Skip to content

Commit d03a8b2

Browse files
jstouracdbasunag
andauthored
test(workbenches): add a test to validate ImageStreams (#1126)
Co-authored-by: Debarati Basu-Nag <dbasunag@redhat.com>
1 parent 85f43fd commit d03a8b2

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""ImageStream health checks for workbench-related images."""
2+
3+
from typing import Any
4+
5+
import pytest
6+
from kubernetes.dynamic import DynamicClient
7+
from ocp_resources.image_stream import ImageStream
8+
from pytest_testconfig import config as py_config
9+
from simple_logger.logger import get_logger
10+
11+
pytestmark = [pytest.mark.smoke]
12+
LOGGER = get_logger(name=__name__)
13+
IMPORT_SUCCESS_CONDITION_TYPE = "ImportSuccess"
14+
15+
16+
def _validate_imagestream_tag_health(
17+
imagestream_name: str,
18+
tag_name: str,
19+
tag_data: dict[str, Any],
20+
) -> list[str]:
21+
"""
22+
Validate one ImageStream status tag and return all discovered errors.
23+
24+
A tag is considered healthy when it has at least one resolved item in
25+
`status.tags[].items`, each item points to a digest-based image reference,
26+
and an optional `ImportSuccess` condition (when present) is `True`.
27+
28+
Args:
29+
imagestream_name: Name of the parent ImageStream (for error reporting).
30+
tag_name: Name of the ImageStream tag being validated.
31+
tag_data: Raw `status.tags[]` payload for the tag.
32+
33+
Returns:
34+
List of validation error messages. Empty list means the tag is healthy.
35+
"""
36+
errors: list[str] = []
37+
38+
raw_tag_items = tag_data.get("items")
39+
tag_items = raw_tag_items if isinstance(raw_tag_items, list) else []
40+
import_conditions = [
41+
condition
42+
for condition in (tag_data.get("conditions") or [])
43+
if condition.get("type") == IMPORT_SUCCESS_CONDITION_TYPE
44+
]
45+
latest_import_condition = (
46+
max(import_conditions, key=lambda condition: condition.get("generation", -1)) if import_conditions else None
47+
)
48+
import_status = latest_import_condition.get("status") if latest_import_condition else "N/A"
49+
LOGGER.info(
50+
f"Checked ImageStream tag {imagestream_name}:{tag_name} "
51+
f"(items_count={len(tag_items)}, import_success={import_status})"
52+
)
53+
54+
# A tag is considered unresolved if no image item exists.
55+
# In that case we expect an ImportSuccess=False condition to explain the failure reason.
56+
if not tag_items:
57+
failure_details = (
58+
"no ImportSuccess condition was reported"
59+
if not latest_import_condition
60+
else (
61+
f"status={latest_import_condition.get('status')}, "
62+
f"reason={latest_import_condition.get('reason')}, "
63+
f"message={latest_import_condition.get('message')}"
64+
)
65+
)
66+
errors.append(
67+
f"ImageStream {imagestream_name} tag {tag_name} has unresolved status.tags.items; "
68+
f"ImportSuccess details: {failure_details}"
69+
)
70+
return errors
71+
72+
for item_index, item in enumerate(tag_items):
73+
docker_image_reference = str(item.get("dockerImageReference", ""))
74+
if "@sha256:" not in docker_image_reference:
75+
errors.append(
76+
f"ImageStream {imagestream_name} tag {tag_name} item #{item_index} "
77+
"has unresolved dockerImageReference: "
78+
f"{docker_image_reference}"
79+
)
80+
81+
image_reference = str(item.get("image", ""))
82+
if not image_reference.startswith("sha256:"):
83+
errors.append(
84+
f"ImageStream {imagestream_name} tag {tag_name} item #{item_index} has unresolved image reference: "
85+
f"{image_reference}"
86+
)
87+
88+
# If the tag resolved to items but ImportSuccess exists and reports failure, this is still an error.
89+
if latest_import_condition and latest_import_condition.get("status") != "True":
90+
errors.append(
91+
f"ImageStream {imagestream_name} tag {tag_name} has resolved items but ImportSuccess is not True: "
92+
f"status={latest_import_condition.get('status')}, "
93+
f"reason={latest_import_condition.get('reason')}, "
94+
f"message={latest_import_condition.get('message')}"
95+
)
96+
97+
return errors
98+
99+
100+
def _validate_imagestreams_with_label(
101+
imagestreams: list[ImageStream],
102+
label_selector: str,
103+
expected_count: int,
104+
) -> None:
105+
"""
106+
Validate ImageStreams selected by label and fail the test if unhealthy.
107+
108+
This helper enforces:
109+
- expected ImageStream count for the selector
110+
- every tag declared in `spec.tags` appears in `status.tags`
111+
- per-tag resolution/import checks via `_validate_imagestream_tag_health`
112+
113+
Args:
114+
imagestreams: ImageStreams fetched for the label selector.
115+
label_selector: Label selector used to fetch ImageStreams.
116+
expected_count: Expected number of matching ImageStreams.
117+
118+
Raises:
119+
pytest.fail: When any validation error is found.
120+
"""
121+
errors: list[str] = []
122+
actual_count = len(imagestreams)
123+
LOGGER.info(
124+
f"Checking ImageStreams for label selector '{label_selector}': "
125+
f"expected_count={expected_count}, actual_count={actual_count}"
126+
)
127+
if imagestreams:
128+
LOGGER.info(
129+
f"ImageStreams matched for '{label_selector}': {', '.join(sorted(is_obj.name for is_obj in imagestreams))}"
130+
)
131+
if actual_count != expected_count:
132+
imagestream_names = ", ".join(sorted(imagestream.name for imagestream in imagestreams))
133+
errors.append(
134+
f"Expected {expected_count} ImageStreams with label '{label_selector}', found {actual_count}. "
135+
f"Found: [{imagestream_names}]"
136+
)
137+
138+
for imagestream in imagestreams:
139+
imagestream_data: dict[str, Any] = imagestream.instance.to_dict()
140+
imagestream_name = imagestream_data.get("metadata", {}).get("name", imagestream.name)
141+
LOGGER.info(f"Validating ImageStream {imagestream_name} (label selector: {label_selector})")
142+
143+
spec_tag_names = {
144+
str(spec_tag.get("name"))
145+
for spec_tag in imagestream_data.get("spec", {}).get("tags", [])
146+
if spec_tag.get("name")
147+
}
148+
status_tags = imagestream_data.get("status", {}).get("tags", [])
149+
status_tag_names = {str(status_tag.get("tag")) for status_tag in status_tags if status_tag.get("tag")}
150+
151+
missing_status_tags = sorted(spec_tag_names - status_tag_names)
152+
LOGGER.info(
153+
f"ImageStream {imagestream_name} tag coverage: "
154+
f"spec_tags={sorted(spec_tag_names)}, status_tags={sorted(status_tag_names)}"
155+
)
156+
errors.extend([
157+
f"ImageStream {imagestream_name} spec tag {missing_tag} is missing from status.tags "
158+
f"(label selector: {label_selector})"
159+
for missing_tag in missing_status_tags
160+
])
161+
162+
for status_tag in status_tags:
163+
tag_name = str(status_tag.get("tag", "<missing-tag-name>"))
164+
errors.extend(
165+
_validate_imagestream_tag_health(
166+
imagestream_name=imagestream_name,
167+
tag_name=tag_name,
168+
tag_data=status_tag,
169+
)
170+
)
171+
172+
if errors:
173+
pytest.fail("\n".join(errors))
174+
175+
176+
@pytest.mark.parametrize(
177+
"label_selector, expected_imagestream_count",
178+
[
179+
pytest.param("opendatahub.io/notebook-image=true", 11, id="notebook_imagestreams"),
180+
pytest.param("opendatahub.io/runtime-image=true", 7, id="runtime_imagestreams"),
181+
],
182+
)
183+
def test_workbench_imagestreams_health(
184+
admin_client: DynamicClient,
185+
label_selector: str,
186+
expected_imagestream_count: int,
187+
) -> None:
188+
"""
189+
Given workbench-related ImageStreams in the applications namespace.
190+
When ImageStreams are listed by the expected workbench labels.
191+
Then all expected ImageStreams exist and each tag is imported and resolved successfully.
192+
"""
193+
imagestreams = list(
194+
ImageStream.get(
195+
client=admin_client,
196+
namespace=py_config["applications_namespace"],
197+
label_selector=label_selector,
198+
)
199+
)
200+
201+
_validate_imagestreams_with_label(
202+
imagestreams=imagestreams,
203+
label_selector=label_selector,
204+
expected_count=expected_imagestream_count,
205+
)

0 commit comments

Comments
 (0)