Skip to content

Commit 1037683

Browse files
authored
OCI registry and example of usage (#493)
* change: remove istio annotations * change: use zot image
1 parent d0127ef commit 1037683

File tree

5 files changed

+281
-0
lines changed

5 files changed

+281
-0
lines changed

tests/conftest.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ocp_resources.mariadb_operator import MariadbOperator
2020
from ocp_resources.node import Node
2121
from ocp_resources.pod import Pod
22+
from ocp_resources.route import Route
2223
from ocp_resources.secret import Secret
2324
from ocp_resources.service import Service
2425
from ocp_resources.subscription import Subscription
@@ -49,6 +50,7 @@
4950
DscComponents,
5051
Labels,
5152
MinIo,
53+
OCIRegistry,
5254
Protocols,
5355
Timeout,
5456
OPENSHIFT_OPERATORS,
@@ -565,6 +567,123 @@ def minio_data_connection(
565567
yield secret
566568

567569

570+
# OCI Registry
571+
@pytest.fixture(scope="class")
572+
def oci_namespace(admin_client: DynamicClient) -> Generator[Namespace, Any, Any]:
573+
with create_ns(
574+
name=f"{OCIRegistry.Metadata.NAME}-{shortuuid.uuid().lower()}",
575+
admin_client=admin_client,
576+
) as ns:
577+
yield ns
578+
579+
580+
@pytest.fixture(scope="class")
581+
def oci_registry_pod_with_minio(
582+
request: FixtureRequest,
583+
admin_client: DynamicClient,
584+
oci_namespace: Namespace,
585+
minio_service: Service,
586+
) -> Generator[Pod, Any, Any]:
587+
pod_labels = {Labels.Openshift.APP: OCIRegistry.Metadata.NAME}
588+
589+
if labels := request.param.get("labels"):
590+
pod_labels.update(labels)
591+
592+
minio_fqdn = f"{minio_service.name}.{minio_service.namespace}.svc.cluster.local"
593+
minio_endpoint = f"{minio_fqdn}:{MinIo.Metadata.DEFAULT_PORT}"
594+
595+
with Pod(
596+
client=admin_client,
597+
name=OCIRegistry.Metadata.NAME,
598+
namespace=oci_namespace.name,
599+
containers=[
600+
{
601+
"args": request.param.get("args"),
602+
"env": [
603+
{"name": "ZOT_STORAGE_STORAGEDRIVER_NAME", "value": OCIRegistry.Storage.STORAGE_DRIVER},
604+
{
605+
"name": "ZOT_STORAGE_STORAGEDRIVER_ROOTDIRECTORY",
606+
"value": OCIRegistry.Storage.STORAGE_DRIVER_ROOT_DIRECTORY,
607+
},
608+
{"name": "ZOT_STORAGE_STORAGEDRIVER_BUCKET", "value": MinIo.Buckets.MODELMESH_EXAMPLE_MODELS},
609+
{"name": "ZOT_STORAGE_STORAGEDRIVER_REGION", "value": OCIRegistry.Storage.STORAGE_DRIVER_REGION},
610+
{"name": "ZOT_STORAGE_STORAGEDRIVER_REGIONENDPOINT", "value": f"http://{minio_endpoint}"},
611+
{"name": "ZOT_STORAGE_STORAGEDRIVER_ACCESSKEY", "value": MinIo.Credentials.ACCESS_KEY_VALUE},
612+
{"name": "ZOT_STORAGE_STORAGEDRIVER_SECRETKEY", "value": MinIo.Credentials.SECRET_KEY_VALUE},
613+
{
614+
"name": "ZOT_STORAGE_STORAGEDRIVER_SECURE",
615+
"value": OCIRegistry.Storage.STORAGE_STORAGEDRIVER_SECURE,
616+
},
617+
{
618+
"name": "ZOT_STORAGE_STORAGEDRIVER_FORCEPATHSTYLE",
619+
"value": OCIRegistry.Storage.STORAGE_STORAGEDRIVER_FORCEPATHSTYLE,
620+
},
621+
{"name": "ZOT_HTTP_ADDRESS", "value": OCIRegistry.Metadata.DEFAULT_HTTP_ADDRESS},
622+
{"name": "ZOT_HTTP_PORT", "value": str(OCIRegistry.Metadata.DEFAULT_PORT)},
623+
{"name": "ZOT_LOG_LEVEL", "value": "info"},
624+
],
625+
"image": request.param.get("image", OCIRegistry.PodConfig.REGISTRY_IMAGE),
626+
"name": OCIRegistry.Metadata.NAME,
627+
"securityContext": {
628+
"allowPrivilegeEscalation": False,
629+
"capabilities": {"drop": ["ALL"]},
630+
"runAsNonRoot": True,
631+
"seccompProfile": {"type": "RuntimeDefault"},
632+
},
633+
"volumeMounts": [
634+
{
635+
"name": "zot-data",
636+
"mountPath": "/var/lib/registry",
637+
}
638+
],
639+
}
640+
],
641+
volumes=[
642+
{
643+
"name": "zot-data",
644+
"emptyDir": {},
645+
}
646+
],
647+
label=pod_labels,
648+
annotations=request.param.get("annotations"),
649+
) as oci_pod:
650+
oci_pod.wait_for_condition(condition="Ready", status="True")
651+
yield oci_pod
652+
653+
654+
@pytest.fixture(scope="class")
655+
def oci_registry_service(admin_client: DynamicClient, oci_namespace: Namespace) -> Generator[Service, Any, Any]:
656+
with Service(
657+
client=admin_client,
658+
name=OCIRegistry.Metadata.NAME,
659+
namespace=oci_namespace.name,
660+
ports=[
661+
{
662+
"name": f"{OCIRegistry.Metadata.NAME}-port",
663+
"port": OCIRegistry.Metadata.DEFAULT_PORT,
664+
"protocol": Protocols.TCP,
665+
"targetPort": OCIRegistry.Metadata.DEFAULT_PORT,
666+
}
667+
],
668+
selector={
669+
Labels.Openshift.APP: OCIRegistry.Metadata.NAME,
670+
},
671+
session_affinity="ClientIP",
672+
) as oci_service:
673+
yield oci_service
674+
675+
676+
@pytest.fixture(scope="class")
677+
def oci_registry_route(admin_client: DynamicClient, oci_registry_service: Service) -> Generator[Route, Any, Any]:
678+
with Route(
679+
client=admin_client,
680+
name=OCIRegistry.Metadata.NAME,
681+
namespace=oci_registry_service.namespace,
682+
service=oci_registry_service.name,
683+
) as oci_route:
684+
yield oci_route
685+
686+
568687
@pytest.fixture(scope="session")
569688
def nodes(admin_client: DynamicClient) -> Generator[list[Node], Any, Any]:
570689
yield list(Node.get(dyn_client=admin_client))

tests/model_registry/async_job/__init__.py

Whitespace-only changes.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import pytest
2+
import json
3+
4+
from ocp_resources.route import Route
5+
6+
from utilities.constants import OCIRegistry, MinIo
7+
from simple_logger.logger import get_logger
8+
from tests.model_registry.async_job.utils import (
9+
push_blob_to_oci_registry,
10+
create_manifest,
11+
push_manifest_to_oci_registry,
12+
pull_manifest_from_oci_registry,
13+
)
14+
15+
LOGGER = get_logger(name=__name__)
16+
17+
18+
@pytest.mark.parametrize(
19+
"minio_pod, oci_registry_pod_with_minio",
20+
[
21+
pytest.param(
22+
MinIo.PodConfig.MODEL_MESH_MINIO_CONFIG,
23+
OCIRegistry.PodConfig.REGISTRY_BASE_CONFIG,
24+
)
25+
],
26+
indirect=True,
27+
)
28+
@pytest.mark.usefixtures("minio_pod", "oci_registry_pod_with_minio", "oci_registry_route")
29+
class TestOciRegistry:
30+
"""
31+
Temporary test for OCI registry deployment functionality using MinIO backend
32+
It will be replaced with a more comprehensive e2e test as part of https://issues.redhat.com/browse/RHOAISTRAT-456
33+
"""
34+
35+
def test_oci_registry_push_and_pull_operations(
36+
self,
37+
oci_registry_route: Route,
38+
) -> None:
39+
"""Test pushing and pulling content to/from the OCI registry with MinIO backend."""
40+
41+
registry_host = oci_registry_route.instance.spec.host
42+
registry_url = f"http://{registry_host}"
43+
44+
LOGGER.info(f"Testing OCI registry at: {registry_url}")
45+
test_repo = "test/simple-artifact"
46+
test_tag = "v1.0"
47+
test_data = b"Hello from OCI registry test! This could be model data stored in MinIO."
48+
49+
blob_digest = push_blob_to_oci_registry(registry_url=registry_url, data=test_data, repo=test_repo)
50+
51+
config_data = {"architecture": "amd64", "os": "linux"}
52+
config_json = json.dumps(config_data, separators=(",", ":")).encode("utf-8")
53+
config_digest = push_blob_to_oci_registry(registry_url=registry_url, data=config_json, repo=test_repo)
54+
55+
manifest = create_manifest(
56+
blob_digest=blob_digest, config_json=config_json, config_digest=config_digest, data=test_data
57+
)
58+
59+
push_manifest_to_oci_registry(registry_url=registry_url, manifest=manifest, repo=test_repo, tag=test_tag)
60+
61+
manifest_get = pull_manifest_from_oci_registry(registry_url=registry_url, repo=test_repo, tag=test_tag)
62+
63+
assert manifest_get["schemaVersion"] == 2
64+
assert manifest_get["config"]["digest"] == config_digest
65+
assert len(manifest_get["layers"]) == 1
66+
assert manifest_get["layers"][0]["digest"] == blob_digest
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import hashlib
2+
import requests
3+
import json
4+
5+
from simple_logger.logger import get_logger
6+
7+
LOGGER = get_logger(name=__name__)
8+
9+
10+
def push_blob_to_oci_registry(registry_url: str, data: bytes, repo: str = "test/simple-artifact") -> str:
11+
"""
12+
Push a blob to an OCI registry.
13+
https://specs.opencontainers.org/distribution-spec/?v=v1.0.0#pushing-blobs
14+
POST to /v2/<repo>/blobs/uploads/ in order to initiate the upload
15+
The response will contain a Location header that contains the upload URL
16+
PUT to the Location URL with the data to be uploaded
17+
"""
18+
19+
blob_digest = f"sha256:{hashlib.sha256(data).hexdigest()}"
20+
21+
LOGGER.info(f"Pushing blob with digest: {blob_digest}")
22+
23+
upload_response = requests.post(f"{registry_url}/v2/{repo}/blobs/uploads/", timeout=10)
24+
LOGGER.info(f"Blob upload initiation: {upload_response.status_code}")
25+
assert upload_response.status_code == 202, f"Failed to initiate blob upload: {upload_response.status_code}"
26+
27+
upload_location = upload_response.headers.get("Location")
28+
LOGGER.info(f"Upload location: {upload_location}")
29+
base_url = f"{registry_url}{upload_location}"
30+
upload_url = f"{base_url}?digest={blob_digest}"
31+
response = requests.put(url=upload_url, data=data, headers={"Content-Type": "application/octet-stream"}, timeout=10)
32+
assert response.status_code == 201, f"Failed to upload blob: {response.status_code}"
33+
return blob_digest
34+
35+
36+
def create_manifest(blob_digest: str, config_json: str, config_digest: str, data: bytes) -> bytes:
37+
"""Create a manifest for an OCI registry."""
38+
39+
manifest = {
40+
"schemaVersion": 2,
41+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
42+
"config": {
43+
"mediaType": "application/vnd.oci.image.config.v1+json",
44+
"size": len(config_json),
45+
"digest": config_digest,
46+
},
47+
"layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar", "size": len(data), "digest": blob_digest}],
48+
}
49+
50+
return json.dumps(manifest, separators=(",", ":")).encode("utf-8")
51+
52+
53+
def push_manifest_to_oci_registry(registry_url: str, manifest: bytes, repo: str, tag: str) -> None:
54+
"""Push a manifest to an OCI registry."""
55+
response = requests.put(
56+
f"{registry_url}/v2/{repo}/manifests/{tag}",
57+
data=manifest,
58+
headers={"Content-Type": "application/vnd.oci.image.manifest.v1+json"},
59+
timeout=10,
60+
)
61+
assert response.status_code == 201, f"Failed to push manifest: {response.status_code}"
62+
63+
64+
def pull_manifest_from_oci_registry(registry_url: str, repo: str, tag: str) -> dict:
65+
"""Pull a manifest from an OCI registry."""
66+
response = requests.get(
67+
f"{registry_url}/v2/{repo}/manifests/{tag}",
68+
headers={"Accept": "application/vnd.oci.image.manifest.v1+json"},
69+
timeout=10,
70+
)
71+
LOGGER.info(f"Manifest pull: {response.status_code}")
72+
assert response.status_code == 200, f"Failed to pull manifest: {response.status_code}"
73+
return response.json()

utilities/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,29 @@ class ModelCarImage:
246246
GRANITE_8B_CODE_INSTRUCT: str = "oci://registry.redhat.io/rhelai1/modelcar-granite-8b-code-instruct:1.4"
247247

248248

249+
class OCIRegistry:
250+
class Metadata:
251+
NAME: str = "oci-registry"
252+
DEFAULT_PORT: int = 5000
253+
DEFAULT_HTTP_ADDRESS: str = "0.0.0.0"
254+
255+
class PodConfig:
256+
REGISTRY_IMAGE: str = "ghcr.io/project-zot/zot-linux-amd64:v2.1.7"
257+
REGISTRY_BASE_CONFIG: dict[str, Any] = {
258+
"args": None,
259+
"labels": {
260+
"maistra.io/expose-route": "true",
261+
},
262+
}
263+
264+
class Storage:
265+
STORAGE_DRIVER: str = "s3"
266+
STORAGE_DRIVER_ROOT_DIRECTORY: str = "/registry"
267+
STORAGE_DRIVER_REGION: str = "us-east-1"
268+
STORAGE_STORAGEDRIVER_SECURE: str = "false"
269+
STORAGE_STORAGEDRIVER_FORCEPATHSTYLE: str = "true"
270+
271+
249272
class MinIo:
250273
class Metadata:
251274
NAME: str = "minio"

0 commit comments

Comments
 (0)