1515from ocp_resources .config_map import ConfigMap
1616from ocp_resources .deployment import Deployment
1717from ocp_resources .namespace import Namespace
18+ from ocp_resources .pod import Pod
19+ from ocp_resources .service import Service
1820from ocp_resources .subscription import Subscription
1921from ocp_utilities .operators import install_operator , uninstall_operator
22+ from pyhelper_utils .shell import run_command
2023from pytest_testconfig import config as py_config
2124from simple_logger .logger import get_logger
25+ from timeout_sampler import TimeoutSampler
2226
2327from tests .model_registry .model_registry .python_client .signing .constants import (
2428 SECURESIGN_API_VERSION ,
2529 SECURESIGN_NAME ,
2630 SECURESIGN_NAMESPACE ,
31+ SIGNING_OCI_REPO_NAME ,
32+ SIGNING_OCI_TAG ,
2733 TAS_CONNECTION_TYPE_NAME ,
2834)
2935from tests .model_registry .model_registry .python_client .signing .utils import (
3339 get_root_checksum ,
3440 get_tas_service_urls ,
3541)
36- from utilities .constants import OPENSHIFT_OPERATORS , Timeout
42+ from utilities .constants import OPENSHIFT_OPERATORS , Labels , ModelCarImage , OCIRegistry , Timeout
3743from utilities .infra import get_openshift_token , is_managed_cluster
44+ from utilities .resources .route import Route
3845from utilities .resources .securesign import Securesign
3946
4047LOGGER = get_logger (name = __name__ )
@@ -314,7 +321,89 @@ def tas_connection_type(admin_client: DynamicClient, securesign_instance: Secure
314321 LOGGER .info (f"TAS Connection Type '{ TAS_CONNECTION_TYPE_NAME } ' deleted from namespace '{ app_namespace } '" )
315322
316323
317- @pytest .fixture (scope = "package" )
324+ @pytest .fixture (scope = "class" )
325+ def oci_registry_pod (
326+ admin_client : DynamicClient ,
327+ oci_namespace : Namespace ,
328+ ) -> Generator [Pod , Any ]:
329+ """Create a simple OCI registry (Zot) pod with local emptyDir storage.
330+
331+ Unlike oci_registry_pod_with_minio, this does not require MinIO — data is
332+ stored in an emptyDir volume, which is sufficient for signing test scenarios.
333+
334+ Args:
335+ admin_client: Kubernetes dynamic client
336+ oci_namespace: Namespace for the OCI registry pod
337+
338+ Yields:
339+ Pod: Ready OCI registry pod
340+ """
341+ with Pod (
342+ client = admin_client ,
343+ name = OCIRegistry .Metadata .NAME ,
344+ namespace = oci_namespace .name ,
345+ containers = [
346+ {
347+ "env" : [
348+ {"name" : "ZOT_HTTP_ADDRESS" , "value" : OCIRegistry .Metadata .DEFAULT_HTTP_ADDRESS },
349+ {"name" : "ZOT_HTTP_PORT" , "value" : str (OCIRegistry .Metadata .DEFAULT_PORT )},
350+ {"name" : "ZOT_LOG_LEVEL" , "value" : "info" },
351+ ],
352+ "image" : OCIRegistry .PodConfig .REGISTRY_IMAGE ,
353+ "name" : OCIRegistry .Metadata .NAME ,
354+ "securityContext" : {
355+ "allowPrivilegeEscalation" : False ,
356+ "capabilities" : {"drop" : ["ALL" ]},
357+ "runAsNonRoot" : True ,
358+ "seccompProfile" : {"type" : "RuntimeDefault" },
359+ },
360+ "volumeMounts" : [
361+ {
362+ "name" : "zot-data" ,
363+ "mountPath" : "/var/lib/registry" ,
364+ }
365+ ],
366+ }
367+ ],
368+ volumes = [
369+ {
370+ "name" : "zot-data" ,
371+ "emptyDir" : {},
372+ }
373+ ],
374+ label = {
375+ Labels .Openshift .APP : OCIRegistry .Metadata .NAME ,
376+ "maistra.io/expose-route" : "true" ,
377+ },
378+ ) as oci_pod :
379+ oci_pod .wait_for_condition (condition = "Ready" , status = "True" )
380+ yield oci_pod
381+
382+
383+ @pytest .fixture (scope = "class" )
384+ def ai_hub_oci_registry_route (admin_client : DynamicClient , oci_registry_service : Service ) -> Generator [Route , Any ]:
385+ """Override the default Route with edge TLS termination.
386+
387+ Cosign requires HTTPS. Edge termination lets the OpenShift router handle TLS
388+ and forward plain HTTP to the Zot backend.
389+ """
390+ with Route (
391+ client = admin_client ,
392+ name = OCIRegistry .Metadata .NAME ,
393+ namespace = oci_registry_service .namespace ,
394+ to = {"kind" : "Service" , "name" : oci_registry_service .name },
395+ tls = {"termination" : "edge" , "insecureEdgeTerminationPolicy" : "Redirect" },
396+ ) as oci_route :
397+ yield oci_route
398+
399+
400+ @pytest .fixture (scope = "class" )
401+ def ai_hub_oci_registry_host (ai_hub_oci_registry_route : Route ) -> str :
402+ """Get the OCI registry host from the route."""
403+ return ai_hub_oci_registry_route .instance .spec .host
404+
405+
406+ @pytest .fixture (scope = "class" )
318407def downloaded_model_dir () -> Path :
319408 """Download a test model from Hugging Face to a temporary directory.
320409
@@ -334,7 +423,7 @@ def downloaded_model_dir() -> Path:
334423 return model_dir
335424
336425
337- @pytest .fixture (scope = "package " )
426+ @pytest .fixture (scope = "class " )
338427def set_environment_variables (securesign_instance : Securesign ) -> Generator [None , Any ]:
339428 """
340429 Create a service account token and save it to a temporary directory.
@@ -403,6 +492,81 @@ def signer(set_environment_variables) -> Signer:
403492 return signer
404493
405494
495+ @pytest .fixture (scope = "class" )
496+ def copied_model_to_oci_registry (
497+ oci_registry_pod : Pod ,
498+ ai_hub_oci_registry_host : str ,
499+ ) -> Generator [str , Any ]:
500+ """Copy ModelCarImage.MNIST_8_1 from quay.io to the local OCI registry using skopeo.
501+
502+ Sets COSIGN_ALLOW_INSECURE_REGISTRY so cosign can access the registry over
503+ the edge-terminated Route with a self-signed certificate.
504+
505+ Args:
506+ oci_registry_pod: OCI registry pod fixture ensuring registry is running
507+ ai_hub_oci_registry_host: OCI registry hostname from route
508+
509+ Yields:
510+ str: The destination image reference with digest (e.g. "{host}/{repo}@sha256:...")
511+ """
512+ # Wait for the OCI registry to be reachable via the Route
513+ registry_url = f"https://{ ai_hub_oci_registry_host } "
514+ LOGGER .info (f"Waiting for OCI registry to be reachable at { registry_url } /v2/" )
515+ for sample in TimeoutSampler (
516+ wait_timeout = 120 ,
517+ sleep = 5 ,
518+ func = requests .get ,
519+ url = f"{ registry_url } /v2/" ,
520+ timeout = 5 ,
521+ verify = False ,
522+ ):
523+ if sample .ok :
524+ LOGGER .info ("OCI registry is reachable" )
525+ break
526+
527+ source_image = ModelCarImage .MNIST_8_1 .removeprefix ("oci://" )
528+ dest_ref = f"{ ai_hub_oci_registry_host } /{ SIGNING_OCI_REPO_NAME } :{ SIGNING_OCI_TAG } "
529+
530+ LOGGER .info (f"Copying image from docker://{ source_image } to docker://{ dest_ref } " )
531+ run_command (
532+ command = [
533+ "skopeo" ,
534+ "copy" ,
535+ "--dest-tls-verify=false" ,
536+ f"docker://{ source_image } " ,
537+ f"docker://{ dest_ref } " ,
538+ ],
539+ check = True ,
540+ )
541+ LOGGER .info (f"Image copied successfully to { dest_ref } " )
542+
543+ # Get the digest of the pushed image
544+ _ , inspect_out , _ = run_command (
545+ command = [
546+ "skopeo" ,
547+ "inspect" ,
548+ "--tls-verify=false" ,
549+ f"docker://{ dest_ref } " ,
550+ ],
551+ check = True ,
552+ )
553+ digest = json .loads (inspect_out ).get ("Digest" , "" )
554+ LOGGER .info (f"Pushed image { inspect_out } digest: { digest } " )
555+
556+ dest_with_digest = f"{ ai_hub_oci_registry_host } /{ SIGNING_OCI_REPO_NAME } @{ digest } "
557+ LOGGER .info (f"Full image reference: { dest_with_digest } " )
558+
559+ # Set cosign env var to allow insecure registry access (self-signed cert from edge Route)
560+ os .environ ["COSIGN_ALLOW_INSECURE_REGISTRY" ] = "true"
561+ LOGGER .info ("Set COSIGN_ALLOW_INSECURE_REGISTRY=true" )
562+
563+ yield dest_with_digest
564+
565+ # Cleanup
566+ os .environ .pop ("COSIGN_ALLOW_INSECURE_REGISTRY" , None )
567+ LOGGER .info ("Cleaned up COSIGN_ALLOW_INSECURE_REGISTRY" )
568+
569+
406570@pytest .fixture (scope = "function" )
407571def signed_model (signer , downloaded_model_dir ) -> Path :
408572 """
@@ -413,13 +577,3 @@ def signed_model(signer, downloaded_model_dir) -> Path:
413577 LOGGER .info ("Model signed successfully" )
414578
415579 return downloaded_model_dir
416-
417-
418- @pytest .fixture (scope = "function" )
419- def verified_model (signer , downloaded_model_dir ) -> None :
420- """
421- Verify a signed model.
422- """
423- LOGGER .info (f"Verifying signed model in directory: { downloaded_model_dir } " )
424- signer .verify_model (model_path = str (downloaded_model_dir ))
425- LOGGER .info ("Model verified successfully" )
0 commit comments