Skip to content

Commit a5ff7e3

Browse files
authored
feat: New tests for image signing and split infra tests to smaller ones (#1222)
Signed-off-by: Debarati Basu-Nag <dbasunag@redhat.com>
1 parent 5e62a40 commit a5ff7e3

File tree

6 files changed

+425
-70
lines changed

6 files changed

+425
-70
lines changed

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ENV PATH="$PATH:$BIN_DIR"
1313

1414
# Install system dependencies using dnf
1515
RUN dnf update -y \
16-
&& dnf install -y python3 python3-pip ssh gnupg curl gpg wget vim httpd-tools rsync openssl openssl-devel\
16+
&& dnf install -y python3 python3-pip ssh gnupg curl gpg wget vim httpd-tools rsync openssl openssl-devel skopeo\
1717
&& dnf clean all \
1818
&& rm -rf /var/cache/dnf
1919

@@ -22,6 +22,10 @@ RUN curl -sSL "https://github.com/fullstorydev/grpcurl/releases/download/v1.9.2/
2222
&& tar xvf /tmp/grpcurl_1.2.tar.gz --no-same-owner \
2323
&& mv grpcurl /usr/bin/grpcurl
2424

25+
# Install cosign
26+
RUN curl -sSL "https://github.com/sigstore/cosign/releases/download/v2.4.2/cosign-linux-amd64" --output /usr/bin/cosign \
27+
&& chmod +x /usr/bin/cosign
28+
2529
RUN useradd -ms /bin/bash $USER
2630
USER $USER
2731
WORKDIR $HOME_DIR

tests/model_registry/model_registry/python_client/signing/conftest.py

Lines changed: 167 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@
1515
from ocp_resources.config_map import ConfigMap
1616
from ocp_resources.deployment import Deployment
1717
from ocp_resources.namespace import Namespace
18+
from ocp_resources.pod import Pod
19+
from ocp_resources.service import Service
1820
from ocp_resources.subscription import Subscription
1921
from ocp_utilities.operators import install_operator, uninstall_operator
22+
from pyhelper_utils.shell import run_command
2023
from pytest_testconfig import config as py_config
2124
from simple_logger.logger import get_logger
25+
from timeout_sampler import TimeoutSampler
2226

2327
from 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
)
2935
from tests.model_registry.model_registry.python_client.signing.utils import (
@@ -33,8 +39,9 @@
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
3743
from utilities.infra import get_openshift_token, is_managed_cluster
44+
from utilities.resources.route import Route
3845
from utilities.resources.securesign import Securesign
3946

4047
LOGGER = 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")
318407
def 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")
338427
def 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")
407571
def 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")

tests/model_registry/model_registry/python_client/signing/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@
99

1010
# TAS Connection Type ConfigMap name
1111
TAS_CONNECTION_TYPE_NAME = "tas-securesign-v1"
12+
13+
# OCI Registry configuration for signed model storage
14+
SIGNING_OCI_REPO_NAME = "signing-test/signed-model"
15+
SIGNING_OCI_TAG = "latest"

0 commit comments

Comments
 (0)