diff --git a/changes/6464.feature.md b/changes/6464.feature.md new file mode 100644 index 00000000000..6b529fe14a2 --- /dev/null +++ b/changes/6464.feature.md @@ -0,0 +1 @@ +Support OCP (OpenShift Container Platform) Registry diff --git a/docs/manager/graphql-reference/schema.graphql b/docs/manager/graphql-reference/schema.graphql index b581080da89..b41414216be 100644 --- a/docs/manager/graphql-reference/schema.graphql +++ b/docs/manager/graphql-reference/schema.graphql @@ -2451,7 +2451,7 @@ type Mutation { ssl_verify: Boolean """ - Added in 24.09.0. Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local'). + Added in 24.09.0. Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local', 'ocp'). """ type: ContainerRegistryTypeField! @@ -2486,7 +2486,7 @@ type Mutation { ssl_verify: Boolean """ - Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local'). Added in 24.09.0. + Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local', 'ocp'). Added in 24.09.0. """ type: ContainerRegistryTypeField diff --git a/docs/manager/graphql-reference/supergraph.graphql b/docs/manager/graphql-reference/supergraph.graphql index ad8bfa49b7b..51bc49c5b20 100644 --- a/docs/manager/graphql-reference/supergraph.graphql +++ b/docs/manager/graphql-reference/supergraph.graphql @@ -4420,7 +4420,7 @@ type Mutation ssl_verify: Boolean """ - Added in 24.09.0. Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local'). + Added in 24.09.0. Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local', 'ocp'). """ type: ContainerRegistryTypeField! @@ -4455,7 +4455,7 @@ type Mutation ssl_verify: Boolean """ - Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local'). Added in 24.09.0. + Registry type. One of ('docker', 'harbor', 'harbor2', 'github', 'gitlab', 'ecr', 'ecr-public', 'local', 'ocp'). Added in 24.09.0. """ type: ContainerRegistryTypeField diff --git a/docs/manager/rest-reference/openapi.json b/docs/manager/rest-reference/openapi.json index 8798995f657..45cb4ce13af 100644 --- a/docs/manager/rest-reference/openapi.json +++ b/docs/manager/rest-reference/openapi.json @@ -51,7 +51,8 @@ "gitlab", "ecr", "ecr-public", - "local" + "local", + "ocp" ], "title": "ContainerRegistryType", "type": "string" diff --git a/fixtures/manager/example-container-registries-ocp.json b/fixtures/manager/example-container-registries-ocp.json new file mode 100644 index 00000000000..932781a6aa8 --- /dev/null +++ b/fixtures/manager/example-container-registries-ocp.json @@ -0,0 +1,13 @@ +{ + "container_registries": [ + { + "id": "7a92d3c1-5f4e-4829-b8e6-2d9c1a3f7e56", + "registry_name": "default-route-openshift-image-registry.apps-crc.testing", + "url": "https://default-route-openshift-image-registry.apps-crc.testing", + "type": "ocp", + "project": "openshift", + "username": "kubeadmin", + "password": "" + } + ] +} diff --git a/src/ai/backend/common/container_registry.py b/src/ai/backend/common/container_registry.py index f072202f498..c493d254423 100644 --- a/src/ai/backend/common/container_registry.py +++ b/src/ai/backend/common/container_registry.py @@ -16,6 +16,7 @@ class ContainerRegistryType(enum.StrEnum): ECR = "ecr" ECR_PUB = "ecr-public" LOCAL = "local" + OCP = "ocp" class AllowedGroupsModel(BaseFieldModel): diff --git a/src/ai/backend/manager/container_registry/__init__.py b/src/ai/backend/manager/container_registry/__init__.py index 9c58a9a7cf1..4469d6f1874 100644 --- a/src/ai/backend/manager/container_registry/__init__.py +++ b/src/ai/backend/manager/container_registry/__init__.py @@ -47,6 +47,10 @@ def get_container_registry_cls(registry_info: ContainerRegistryRow) -> Type[Base from .local import LocalRegistry cr_cls = LocalRegistry + elif registry_type == ContainerRegistryType.OCP: + from .ocp import OpenShiftPlatformContainerRegistry + + cr_cls = OpenShiftPlatformContainerRegistry else: raise RuntimeError(f"Unsupported registry type: {registry_type}") return cr_cls diff --git a/src/ai/backend/manager/container_registry/base.py b/src/ai/backend/manager/container_registry/base.py index a90a0808e66..c67147eed67 100644 --- a/src/ai/backend/manager/container_registry/base.py +++ b/src/ai/backend/manager/container_registry/base.py @@ -479,18 +479,17 @@ async def _process_oci_manifest( if (reporter := progress_reporter.get()) is not None: reporter.total_progress += 1 - async with concurrency_sema.get(): - config_digest = image_info["config"]["digest"] - size_bytes = ( - sum(layer["size"] for layer in image_info["layers"]) + image_info["config"]["size"] - ) + config_digest = image_info["config"]["digest"] + size_bytes = ( + sum(layer["size"] for layer in image_info["layers"]) + image_info["config"]["size"] + ) - async with sess.get( - self.registry_url / f"v2/{image}/blobs/{config_digest}", - **rqst_args, - ) as resp: - resp.raise_for_status() - config_data = await read_json(resp) + async with sess.get( + self.registry_url / f"v2/{image}/blobs/{config_digest}", + **rqst_args, + ) as resp: + resp.raise_for_status() + config_data = await read_json(resp) labels = {} if _config_labels := (config_data.get("config") or {}).get("Labels"): diff --git a/src/ai/backend/manager/container_registry/ocp.py b/src/ai/backend/manager/container_registry/ocp.py new file mode 100644 index 00000000000..ad684c5f05a --- /dev/null +++ b/src/ai/backend/manager/container_registry/ocp.py @@ -0,0 +1,61 @@ +import logging +from http import HTTPStatus +from typing import AsyncIterator, Optional, cast, override + +import aiohttp +import yarl + +from ai.backend.common.docker import login as registry_login +from ai.backend.common.json import read_json +from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.exceptions import ContainerRegistryProjectEmpty + +from .base import BaseContainerRegistry + +log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore[name-defined] + + +class OpenShiftPlatformContainerRegistry(BaseContainerRegistry): + @override + async def fetch_repositories( + self, + sess: aiohttp.ClientSession, + ) -> AsyncIterator[str]: + if not self.registry_info.project: + raise ContainerRegistryProjectEmpty(self.registry_info.type, self.registry_info.project) + + # OpenShift Container Registry uses Docker Registry API v2 + # The credential should have the catalog search privilege. + rqst_args = await registry_login( + sess, + self.registry_url, + self.credentials, + "registry:catalog:*", + ) + catalog_url: Optional[yarl.URL] + catalog_url = (self.registry_url / "v2/_catalog").with_query( + {"n": "30"}, + ) + while catalog_url is not None: + async with sess.get(catalog_url, **rqst_args) as resp: + if resp.status == HTTPStatus.OK: + data = await read_json(resp) + + for item in data["repositories"]: + if item.startswith(self.registry_info.project): + yield item + log.debug("found {} repositories", len(data["repositories"])) + else: + log.warning( + "OpenShift Container Registry {0} does not allow/support catalog search. (status={1})", + self.registry_url, + resp.status, + ) + break + catalog_url = None + next_page_link = resp.links.get("next") + if next_page_link: + next_page_url = cast(yarl.URL, next_page_link["url"]) + catalog_url = self.registry_url.with_path(next_page_url.path).with_query( + next_page_url.query + ) diff --git a/src/ai/backend/manager/models/alembic/versions/b0fb0eb6b6bc_remove_container_registries_registry_.py b/src/ai/backend/manager/models/alembic/versions/b0fb0eb6b6bc_remove_container_registries_registry_.py new file mode 100644 index 00000000000..beb50304e1b --- /dev/null +++ b/src/ai/backend/manager/models/alembic/versions/b0fb0eb6b6bc_remove_container_registries_registry_.py @@ -0,0 +1,38 @@ +"""Remove container_registries.registry_name length restriction + +Revision ID: b0fb0eb6b6bc +Revises: d811b103dbfc +Create Date: 2025-10-29 10:38:05.866930 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b0fb0eb6b6bc" +down_revision = "d811b103dbfc" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.alter_column( + "container_registries", + "registry_name", + existing_type=sa.String(length=50), + type_=sa.String(), + existing_nullable=False, + existing_server_default=None, + ) + + +def downgrade() -> None: + op.alter_column( + "container_registries", + "registry_name", + existing_type=sa.String(), + type_=sa.String(length=50), + existing_nullable=False, + existing_server_default=None, + ) diff --git a/src/ai/backend/manager/models/container_registry.py b/src/ai/backend/manager/models/container_registry.py index 17f6d78770c..ca39eb7129b 100644 --- a/src/ai/backend/manager/models/container_registry.py +++ b/src/ai/backend/manager/models/container_registry.py @@ -106,7 +106,7 @@ class ContainerRegistryRow(Base): __tablename__ = "container_registries" id = IDColumn() url = sa.Column("url", sa.String(length=512), index=True, nullable=False) - registry_name = sa.Column("registry_name", sa.String(length=50), index=True, nullable=False) + registry_name = sa.Column("registry_name", sa.String(), index=True, nullable=False) type = sa.Column( "type", StrEnumType(ContainerRegistryType),