|
1 | 1 | import base64 |
| 2 | +import re |
| 3 | +from typing import List, Tuple |
2 | 4 |
|
3 | 5 | from kubernetes.dynamic import DynamicClient |
| 6 | +from kubernetes.dynamic.exceptions import ResourceNotFoundError |
4 | 7 | from ocp_resources.inference_service import InferenceService |
5 | 8 | from ocp_resources.pod import Pod |
6 | 9 | from simple_logger.logger import get_logger |
7 | 10 |
|
8 | 11 | import utilities.infra |
9 | 12 | from utilities.constants import Annotations, KServeDeploymentType, MODELMESH_SERVING |
| 13 | +from utilities.exceptions import UnexpectedResourceCountError |
| 14 | +from ocp_resources.resource import Resource |
| 15 | +from timeout_sampler import retry |
| 16 | + |
| 17 | +# Constants for image validation |
| 18 | +SHA256_DIGEST_PATTERN = r"@sha256:[a-f0-9]{64}$" |
10 | 19 |
|
11 | 20 | LOGGER = get_logger(name=__name__) |
12 | 21 |
|
@@ -171,3 +180,110 @@ def create_isvc_label_selector_str(isvc: InferenceService, resource_type: str, r |
171 | 180 |
|
172 | 181 | else: |
173 | 182 | raise ValueError(f"Unknown deployment mode {deployment_mode}") |
| 183 | + |
| 184 | + |
| 185 | +def get_pod_images(pod: Pod) -> List[str]: |
| 186 | + """Get all container images from a pod. |
| 187 | +
|
| 188 | + Args: |
| 189 | + pod: The pod to get images from |
| 190 | +
|
| 191 | + Returns: |
| 192 | + List of container image strings |
| 193 | + """ |
| 194 | + return [container.image for container in pod.instance.spec.containers] |
| 195 | + |
| 196 | + |
| 197 | +def validate_image_format(image: str) -> Tuple[bool, str]: |
| 198 | + """Validate image format according to requirements. |
| 199 | +
|
| 200 | + Args: |
| 201 | + image: The image string to validate |
| 202 | +
|
| 203 | + Returns: |
| 204 | + Tuple of (is_valid, error_message) |
| 205 | + """ |
| 206 | + if not image.startswith(Resource.ApiGroup.IMAGE_REGISTRY): |
| 207 | + return False, f"Image {image} is not from {Resource.ApiGroup.IMAGE_REGISTRY}" |
| 208 | + |
| 209 | + if not re.search(SHA256_DIGEST_PATTERN, image): |
| 210 | + return False, f"Image {image} does not use sha256 digest" |
| 211 | + |
| 212 | + return True, "" |
| 213 | + |
| 214 | + |
| 215 | +@retry( |
| 216 | + wait_timeout=60, |
| 217 | + sleep=5, |
| 218 | + exceptions_dict={ResourceNotFoundError: [], UnexpectedResourceCountError: []}, |
| 219 | +) |
| 220 | +def wait_for_pods_by_labels( |
| 221 | + admin_client: DynamicClient, |
| 222 | + namespace: str, |
| 223 | + label_selector: str, |
| 224 | + expected_num_pods: int, |
| 225 | +) -> list[Pod]: |
| 226 | + """ |
| 227 | + Get pods by label selector in a namespace. |
| 228 | +
|
| 229 | + Args: |
| 230 | + admin_client: The admin client to use for pod retrieval |
| 231 | + namespace: The namespace to search in |
| 232 | + label_selector: The label selector to filter pods |
| 233 | + expected_num_pods: The expected number of pods to be found |
| 234 | + Returns: |
| 235 | + List of matching pods |
| 236 | +
|
| 237 | + Raises: |
| 238 | + ResourceNotFoundError: If no pods are found |
| 239 | + """ |
| 240 | + pods = list( |
| 241 | + Pod.get( |
| 242 | + dyn_client=admin_client, |
| 243 | + namespace=namespace, |
| 244 | + label_selector=label_selector, |
| 245 | + ) |
| 246 | + ) |
| 247 | + if not pods: |
| 248 | + raise ResourceNotFoundError(f"No pods found with label selector {label_selector} in namespace {namespace}") |
| 249 | + if len(pods) != expected_num_pods: |
| 250 | + raise UnexpectedResourceCountError(f"Expected {expected_num_pods} pods, found {len(pods)}") |
| 251 | + return pods |
| 252 | + |
| 253 | + |
| 254 | +def validate_container_images( |
| 255 | + pod: Pod, |
| 256 | + valid_image_refs: set[str], |
| 257 | + skip_patterns: list[str] | None = None, |
| 258 | +) -> list[str]: |
| 259 | + """ |
| 260 | + Validate all container images in a pod against a set of valid image references. |
| 261 | +
|
| 262 | + Args: |
| 263 | + pod: The pod whose images to validate |
| 264 | + valid_image_refs: Set of valid image references to check against |
| 265 | + skip_patterns: List of patterns to skip validation for (e.g. ["openshift-service-mesh"]) |
| 266 | +
|
| 267 | + Returns: |
| 268 | + List of validation error messages, empty if all validations pass |
| 269 | + """ |
| 270 | + validation_errors = [] |
| 271 | + skip_patterns = skip_patterns or [] |
| 272 | + |
| 273 | + pod_images = get_pod_images(pod=pod) |
| 274 | + for image in pod_images: |
| 275 | + # Skip images matching any skip patterns |
| 276 | + if any(pattern in image for pattern in skip_patterns): |
| 277 | + LOGGER.warning(f"Skipping image {image} as it matches skip patterns") |
| 278 | + continue |
| 279 | + |
| 280 | + # Validate image format |
| 281 | + is_valid, error_msg = validate_image_format(image=image) |
| 282 | + if not is_valid: |
| 283 | + validation_errors.append(f"Pod {pod.name} image validation failed: {error_msg}") |
| 284 | + |
| 285 | + # Check if image is in valid references |
| 286 | + if image not in valid_image_refs: |
| 287 | + validation_errors.append(f"Pod {pod.name} image {image} is not in valid image references") |
| 288 | + |
| 289 | + return validation_errors |
0 commit comments