From 69a36e4c723bb8b1aa6c95ec5559858438cf455a Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 05:39:36 +0000 Subject: [PATCH 01/21] feat: Implement `AssociateContainerRegistryWithGroup`, `DisassociateContainerRegistryWithGroup` --- src/ai/backend/manager/api/schema.graphql | 18 +++++ src/ai/backend/manager/models/gql.py | 11 +++ .../models/gql_models/container_registry.py | 72 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/ai/backend/manager/models/gql_models/container_registry.py diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index cfdf9a29ea..8c44257d9a 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1848,6 +1848,12 @@ type Mutations { """Object id. Can be either global id or object id. Added in 24.09.0.""" id: String! ): DeleteContainerRegistryNode + + """Added in 24.12.0""" + associate_container_registry_with_group(group_id: String!, registry_id: String!): AssociateContainerRegistryWithGroup + + """Added in 24.12.0""" + disassociate_container_registry_with_group(group_id: String!, registry_id: String!): DisassociateContainerRegistryWithGroup create_container_registry(hostname: String!, props: CreateContainerRegistryInput!): CreateContainerRegistry modify_container_registry(hostname: String!, props: ModifyContainerRegistryInput!): ModifyContainerRegistry delete_container_registry(hostname: String!): DeleteContainerRegistry @@ -2581,6 +2587,18 @@ type DeleteContainerRegistryNode { container_registry: ContainerRegistryNode } +"""Added in 24.12.0.""" +type AssociateContainerRegistryWithGroup { + ok: Boolean + msg: String +} + +"""Added in 24.12.0.""" +type DisassociateContainerRegistryWithGroup { + ok: Boolean + msg: String +} + type CreateContainerRegistry { container_registry: ContainerRegistry } diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index ba3a84fa15..ff6c512d85 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -74,6 +74,10 @@ AgentSummaryList, ModifyAgent, ) +from .gql_models.container_registry import ( + AssociateContainerRegistryWithGroup, + DisassociateContainerRegistryWithGroup, +) from .gql_models.domain import ( CreateDomainNode, DomainConnection, @@ -335,6 +339,13 @@ class Mutations(graphene.ObjectType): description="Added in 24.09.0." ) + associate_container_registry_with_group = AssociateContainerRegistryWithGroup.Field( + description="Added in 24.12.0" + ) + disassociate_container_registry_with_group = DisassociateContainerRegistryWithGroup.Field( + description="Added in 24.12.0" + ) + # Legacy mutations create_container_registry = CreateContainerRegistry.Field() modify_container_registry = ModifyContainerRegistry.Field() diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py new file mode 100644 index 0000000000..030e64db1d --- /dev/null +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import logging +from typing import Self + +import graphene +import sqlalchemy as sa + +from ai.backend.common.logging_utils import BraceStyleAdapter +from ai.backend.manager.models.association_container_registries_groups import ( + AssociationContainerRegistriesGroupsRow, +) +from ai.backend.manager.models.base import simple_db_mutate + +from .user import UserRole + +log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore + + +class AssociateContainerRegistryWithGroup(graphene.Mutation): + """Added in 24.12.0.""" + + allowed_roles = (UserRole.SUPERADMIN,) + + class Arguments: + registry_id = graphene.String(required=True) + group_id = graphene.String(required=True) + + ok = graphene.Boolean() + msg = graphene.String() + + @classmethod + async def mutate( + cls, + root, + info: graphene.ResolveInfo, + registry_id: str, + group_id: str, + ) -> Self: + insert_query = sa.insert(AssociationContainerRegistriesGroupsRow).values({ + "registry_id": registry_id, + "group_id": group_id, + }) + return await simple_db_mutate(cls, info.context, insert_query) + + +class DisassociateContainerRegistryWithGroup(graphene.Mutation): + """Added in 24.12.0.""" + + allowed_roles = (UserRole.SUPERADMIN,) + + class Arguments: + registry_id = graphene.String(required=True) + group_id = graphene.String(required=True) + + ok = graphene.Boolean() + msg = graphene.String() + + @classmethod + async def mutate( + cls, + root, + info: graphene.ResolveInfo, + registry_id: str, + group_id: str, + ) -> Self: + delete_query = ( + sa.delete(AssociationContainerRegistriesGroupsRow) + .where(AssociationContainerRegistriesGroupsRow.registry_id == registry_id) + .where(AssociationContainerRegistriesGroupsRow.group_id == group_id) + ) + return await simple_db_mutate(cls, info.context, delete_query) From 8f62d83f94102cd1742f06b877301a45636bb4cb Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 05:49:25 +0000 Subject: [PATCH 02/21] fix: import statement --- src/ai/backend/manager/models/gql_models/container_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py index 030e64db1d..a43bea2215 100644 --- a/src/ai/backend/manager/models/gql_models/container_registry.py +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -6,7 +6,7 @@ import graphene import sqlalchemy as sa -from ai.backend.common.logging_utils import BraceStyleAdapter +from ai.backend.logging import BraceStyleAdapter from ai.backend.manager.models.association_container_registries_groups import ( AssociationContainerRegistriesGroupsRow, ) From 9637892b3fcd08707f2046a1e3e4ed2610a8ea87 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 05:52:26 +0000 Subject: [PATCH 03/21] fix: import statements --- .../manager/models/gql_models/container_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py index a43bea2215..ae44bef0e2 100644 --- a/src/ai/backend/manager/models/gql_models/container_registry.py +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -7,12 +7,12 @@ import sqlalchemy as sa from ai.backend.logging import BraceStyleAdapter -from ai.backend.manager.models.association_container_registries_groups import ( + +from ..association_container_registries_groups import ( AssociationContainerRegistriesGroupsRow, ) -from ai.backend.manager.models.base import simple_db_mutate - -from .user import UserRole +from ..base import simple_db_mutate +from ..user import UserRole log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore From 5954d42514455d9482afb29f346d2e2d0101d7c6 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 06:06:20 +0000 Subject: [PATCH 04/21] chore: Add news fragment --- changes/3067.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/3067.feature.md diff --git a/changes/3067.feature.md b/changes/3067.feature.md new file mode 100644 index 0000000000..5a8055600e --- /dev/null +++ b/changes/3067.feature.md @@ -0,0 +1 @@ +Implement APIs for associating, disassociating `container_registries` with `groups`. From 991d1a829c5bebee8506b4c931be09634703cff9 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 06:28:22 +0000 Subject: [PATCH 05/21] feat: Add APIs in client sdk --- .../backend/client/func/container_registry.py | 72 +++++++++++++++++++ src/ai/backend/client/session.py | 3 + 2 files changed, 75 insertions(+) create mode 100644 src/ai/backend/client/func/container_registry.py diff --git a/src/ai/backend/client/func/container_registry.py b/src/ai/backend/client/func/container_registry.py new file mode 100644 index 0000000000..cff1ac4bee --- /dev/null +++ b/src/ai/backend/client/func/container_registry.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import textwrap + +from ..session import api_session +from .base import BaseFunction, api_function + +__all__ = ("ContainerRegistry",) + + +class ContainerRegistry(BaseFunction): + """ + Provides a shortcut of :func:`Admin.query() + ` that fetches, modifies various container registry + information. + + .. note:: + + All methods in this function class require your API access key to + have the *admin* privilege. + """ + + registry_id: str + + def __init__(self, registry_id: str): + self.registry_id = registry_id + + @api_function + @classmethod + async def associate_group(cls, registry_id: str, group_id: str) -> dict: + """ + Associate container_registry with group. + + :param registry_id: The id of a container registry. + :param group_id: The id of a group. + """ + query = textwrap.dedent( + """\ + mutation($registry_id: String!, $group_id: String!) { + associate_container_registry_with_group( + registry_id: $registry_id, group_id: $group_id) { + ok msg + } + } + """ + ) + variables = {"registry_id": registry_id, "group_id": group_id} + data = await api_session.get().Admin._query(query, variables) + return data["associate_container_registry_with_group"] + + @api_function + @classmethod + async def disassociate_group(cls, registry_id: str, group_id: str) -> dict: + """ + Disassociate container_registry with group. + + :param registry_id: The id of a container registry. + :param group_id: The id of a group. + """ + query = textwrap.dedent( + """\ + mutation($registry_id: String!, $group_id: String!) { + disassociate_container_registry_with_group( + registry_id: $registry_id, group_id: $group_id) { + ok msg + } + } + """ + ) + variables = {"registry_id": registry_id, "group_id": group_id} + data = await api_session.get().Admin._query(query, variables) + return data["disassociate_container_registry_with_group"] diff --git a/src/ai/backend/client/session.py b/src/ai/backend/client/session.py index 7723a62650..53423c825e 100644 --- a/src/ai/backend/client/session.py +++ b/src/ai/backend/client/session.py @@ -254,6 +254,7 @@ class BaseSession(metaclass=abc.ABCMeta): "ScalingGroup", "Storage", "Image", + "ContainerRegistry", "ComputeSession", "SessionTemplate", "Domain", @@ -298,6 +299,7 @@ def __init__( from .func.agent import Agent, AgentWatcher from .func.auth import Auth from .func.bgtask import BackgroundTask + from .func.container_registry import ContainerRegistry from .func.domain import Domain from .func.dotfile import Dotfile from .func.etcd import EtcdConfig @@ -327,6 +329,7 @@ def __init__( self.Storage = Storage self.Auth = Auth self.BackgroundTask = BackgroundTask + self.ContainerRegistry = ContainerRegistry self.EtcdConfig = EtcdConfig self.Domain = Domain self.Group = Group From b5724092b43abc899bb02693f3f050119cb33ff5 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Mon, 11 Nov 2024 08:44:02 +0000 Subject: [PATCH 06/21] fix: Remove useless constructor --- src/ai/backend/client/func/container_registry.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ai/backend/client/func/container_registry.py b/src/ai/backend/client/func/container_registry.py index cff1ac4bee..a9d2dcf6a2 100644 --- a/src/ai/backend/client/func/container_registry.py +++ b/src/ai/backend/client/func/container_registry.py @@ -20,11 +20,6 @@ class ContainerRegistry(BaseFunction): have the *admin* privilege. """ - registry_id: str - - def __init__(self, registry_id: str): - self.registry_id = registry_id - @api_function @classmethod async def associate_group(cls, registry_id: str, group_id: str) -> dict: From 822f6d3ef13fc5a683acd6294f4b14773252e219 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Tue, 12 Nov 2024 04:10:21 +0000 Subject: [PATCH 07/21] chore: Update comment --- src/ai/backend/client/func/container_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/client/func/container_registry.py b/src/ai/backend/client/func/container_registry.py index a9d2dcf6a2..6c49d07aab 100644 --- a/src/ai/backend/client/func/container_registry.py +++ b/src/ai/backend/client/func/container_registry.py @@ -26,8 +26,8 @@ async def associate_group(cls, registry_id: str, group_id: str) -> dict: """ Associate container_registry with group. - :param registry_id: The id of a container registry. - :param group_id: The id of a group. + :param registry_id: Id of the container registry. + :param group_id: Id of the group. """ query = textwrap.dedent( """\ @@ -49,8 +49,8 @@ async def disassociate_group(cls, registry_id: str, group_id: str) -> dict: """ Disassociate container_registry with group. - :param registry_id: The id of a container registry. - :param group_id: The id of a group. + :param registry_id: Id of the container registry. + :param group_id: Id of the group. """ query = textwrap.dedent( """\ From ef72eaa54e9f072441caa1c320d4c3a4b8f8474a Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Tue, 12 Nov 2024 04:11:06 +0000 Subject: [PATCH 08/21] chore: Update comment --- src/ai/backend/client/func/container_registry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/client/func/container_registry.py b/src/ai/backend/client/func/container_registry.py index 6c49d07aab..570ed80237 100644 --- a/src/ai/backend/client/func/container_registry.py +++ b/src/ai/backend/client/func/container_registry.py @@ -26,8 +26,8 @@ async def associate_group(cls, registry_id: str, group_id: str) -> dict: """ Associate container_registry with group. - :param registry_id: Id of the container registry. - :param group_id: Id of the group. + :param registry_id: ID of the container registry. + :param group_id: ID of the group. """ query = textwrap.dedent( """\ @@ -49,8 +49,8 @@ async def disassociate_group(cls, registry_id: str, group_id: str) -> dict: """ Disassociate container_registry with group. - :param registry_id: Id of the container registry. - :param group_id: Id of the group. + :param registry_id: ID of the container registry. + :param group_id: ID of the group. """ query = textwrap.dedent( """\ From cb6c7984190ef3ca8b1f404c831a3750bc3fa845 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Tue, 12 Nov 2024 05:15:11 +0000 Subject: [PATCH 09/21] feat: Implement REST API for container_registry --- .../backend/manager/api/container_registry.py | 88 +++++++++++++++++++ src/ai/backend/manager/server.py | 1 + 2 files changed, 89 insertions(+) create mode 100644 src/ai/backend/manager/api/container_registry.py diff --git a/src/ai/backend/manager/api/container_registry.py b/src/ai/backend/manager/api/container_registry.py new file mode 100644 index 0000000000..0e6fac9bea --- /dev/null +++ b/src/ai/backend/manager/api/container_registry.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any, Iterable, Tuple + +import aiohttp_cors +import sqlalchemy as sa +import trafaret as t +from aiohttp import web + +from ai.backend.common import validators as tx +from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.models.association_container_registries_groups import ( + AssociationContainerRegistriesGroupsRow, +) + +if TYPE_CHECKING: + from .context import RootContext + +from .auth import superadmin_required +from .manager import READ_ALLOWED, server_status_required +from .types import CORSOptions, WebMiddleware +from .utils import check_api_params + +log = BraceStyleAdapter(logging.getLogger(__spec__.name)) + + +@server_status_required(READ_ALLOWED) +@superadmin_required +@check_api_params( + t.Dict({ + tx.AliasedKey(["registry_id", "registry"]): t.String, + tx.AliasedKey(["group_id", "group"]): t.String, + }) +) +async def associate_with_group(request: web.Request, params: Any) -> web.Response: + log.info("ASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params["registry_id"], params["group_id"]) + root_ctx: RootContext = request.app["_root.context"] + registry_id = params["registry_id"] + group_id = params["group_id"] + + async with root_ctx.db.begin_session() as db_sess: + insert_query = sa.insert(AssociationContainerRegistriesGroupsRow).values({ + "registry_id": registry_id, + "group_id": group_id, + }) + + await db_sess.execute(insert_query) + + return web.json_response({}) + + +@server_status_required(READ_ALLOWED) +@superadmin_required +@check_api_params( + t.Dict({ + tx.AliasedKey(["registry_id", "registry"]): t.String, + tx.AliasedKey(["group_id", "group"]): t.String, + }) +) +async def disassociate_with_group(request: web.Request, params: Any) -> web.Response: + log.info("DISASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params["registry_id"], params["group_id"]) + root_ctx: RootContext = request.app["_root.context"] + registry_id = params["registry_id"] + group_id = params["group_id"] + + async with root_ctx.db.begin_session() as db_sess: + delete_query = ( + sa.delete(AssociationContainerRegistriesGroupsRow) + .where(AssociationContainerRegistriesGroupsRow.registry_id == registry_id) + .where(AssociationContainerRegistriesGroupsRow.group_id == group_id) + ) + + await db_sess.execute(delete_query) + + return web.json_response({}) + + +def create_app( + default_cors_options: CORSOptions, +) -> Tuple[web.Application, Iterable[WebMiddleware]]: + app = web.Application() + app["api_versions"] = (1, 2, 3, 4, 5) + app["prefix"] = "container-registries" + cors = aiohttp_cors.setup(app, defaults=default_cors_options) + cors.add(app.router.add_route("POST", "/associate-with-group", associate_with_group)) + cors.add(app.router.add_route("POST", "/disassociate-with-group", disassociate_with_group)) + return app, [] diff --git a/src/ai/backend/manager/server.py b/src/ai/backend/manager/server.py index c695a4e131..1cec8d8692 100644 --- a/src/ai/backend/manager/server.py +++ b/src/ai/backend/manager/server.py @@ -170,6 +170,7 @@ global_subapp_pkgs: Final[list[str]] = [ ".acl", + ".container_registry", ".etcd", ".events", ".auth", From 26756587442604825067311790c21a22d09f7174 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Wed, 4 Dec 2024 06:22:28 +0000 Subject: [PATCH 10/21] fix: Add extra_fixtures --- tests/manager/conftest.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/manager/conftest.py b/tests/manager/conftest.py index 51233ca973..48e5be316b 100644 --- a/tests/manager/conftest.py +++ b/tests/manager/conftest.py @@ -28,6 +28,7 @@ from unittest.mock import AsyncMock, MagicMock from urllib.parse import quote_plus as urlquote +import aiofiles.os import aiohttp import asyncpg import pytest @@ -420,7 +421,12 @@ async def database_engine(local_config, database): @pytest.fixture() -def database_fixture(local_config, test_db, database) -> Iterator[None]: +def extra_fixtures(): + return {} + + +@pytest.fixture() +def database_fixture(local_config, test_db, database, extra_fixtures) -> Iterator[None]: """ Populate the example data as fixtures to the database and delete them after use. @@ -431,12 +437,20 @@ def database_fixture(local_config, test_db, database) -> Iterator[None]: db_url = f"postgresql+asyncpg://{db_user}:{urlquote(db_pass)}@{db_addr}/{test_db}" build_root = Path(os.environ["BACKEND_BUILD_ROOT"]) + + extra_fixture_file = tempfile.NamedTemporaryFile(delete=False) + extra_fixture_file_path = Path(extra_fixture_file.name) + + with open(extra_fixture_file_path, "w") as f: + json.dump(extra_fixtures, f) + fixture_paths = [ build_root / "fixtures" / "manager" / "example-users.json", build_root / "fixtures" / "manager" / "example-keypairs.json", build_root / "fixtures" / "manager" / "example-set-user-main-access-keys.json", build_root / "fixtures" / "manager" / "example-resource-presets.json", build_root / "fixtures" / "manager" / "example-container-registries-harbor.json", + extra_fixture_file_path, ] async def init_fixture() -> None: @@ -461,6 +475,9 @@ async def init_fixture() -> None: yield async def clean_fixture() -> None: + if extra_fixture_file_path.exists(): + await aiofiles.os.remove(extra_fixture_file_path) + engine: SAEngine = sa.ext.asyncio.create_async_engine( db_url, connect_args=pgsql_connect_opts, From be687661d8b5f55b0a5151eca154904a5f0c9ccc Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Wed, 4 Dec 2024 06:22:35 +0000 Subject: [PATCH 11/21] feat: Add `test_associate_container_registry_with_group` --- tests/manager/models/gql_models/BUILD | 1 + .../gql_models/test_container_registries.py | 150 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 tests/manager/models/gql_models/BUILD create mode 100644 tests/manager/models/gql_models/test_container_registries.py diff --git a/tests/manager/models/gql_models/BUILD b/tests/manager/models/gql_models/BUILD new file mode 100644 index 0000000000..75b8f46de9 --- /dev/null +++ b/tests/manager/models/gql_models/BUILD @@ -0,0 +1 @@ +python_tests(name="tests") diff --git a/tests/manager/models/gql_models/test_container_registries.py b/tests/manager/models/gql_models/test_container_registries.py new file mode 100644 index 0000000000..87b1f61ad3 --- /dev/null +++ b/tests/manager/models/gql_models/test_container_registries.py @@ -0,0 +1,150 @@ +import pytest +from graphene import Schema +from graphene.test import Client + +from ai.backend.manager.models.gql import GraphQueryContext, Mutations, Queries +from ai.backend.manager.models.utils import ExtendedAsyncSAEngine +from ai.backend.manager.server import database_ctx + + +@pytest.fixture(scope="module") +def client() -> Client: + return Client(Schema(query=Queries, mutation=Mutations, auto_camelcase=False)) + + +def get_graphquery_context(database_engine: ExtendedAsyncSAEngine) -> GraphQueryContext: + return GraphQueryContext( + schema=None, # type: ignore + dataloader_manager=None, # type: ignore + local_config=None, # type: ignore + shared_config=None, # type: ignore + etcd=None, # type: ignore + user={"domain": "default", "role": "superadmin"}, + access_key="AKIAIOSFODNN7EXAMPLE", + db=database_engine, # type: ignore + redis_stat=None, # type: ignore + redis_image=None, # type: ignore + redis_live=None, # type: ignore + manager_status=None, # type: ignore + known_slot_types=None, # type: ignore + background_task_manager=None, # type: ignore + storage_manager=None, # type: ignore + registry=None, # type: ignore + idle_checker_host=None, # type: ignore + ) + + +FIXTURES_WITH_NOASSOC = [ + { + "groups": [ + { + "id": "00000000-0000-0000-0000-000000000001", + "name": "mock_group", + "description": "", + "is_active": True, + "domain_name": "default", + "resource_policy": "default", + "total_resource_slots": {}, + "allowed_vfolder_hosts": {}, + "type": "general", + } + ], + "container_registries": [ + { + "id": "00000000-0000-0000-0000-000000000002", + "url": "https://mock.registry.com", + "type": "docker", + "project": "mock_project", + "registry_name": "mock_registry", + } + ], + } +] + +FIXTURES_WITH_ASSOC = [ + { + "groups": [ + { + "id": "00000000-0000-0000-0000-000000000001", + "name": "mock_group", + "description": "", + "is_active": True, + "domain_name": "default", + "resource_policy": "default", + "total_resource_slots": {}, + "allowed_vfolder_hosts": {}, + "type": "general", + } + ], + "container_registries": [ + { + "id": "00000000-0000-0000-0000-000000000002", + "url": "https://mock.registry.com", + "type": "docker", + "project": "mock_project", + "registry_name": "mock_registry", + } + ], + "association_container_registries_groups": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + } + ], + } +] + + +@pytest.mark.dependency() +@pytest.mark.asyncio +@pytest.mark.parametrize( + "extra_fixtures", + FIXTURES_WITH_NOASSOC + FIXTURES_WITH_ASSOC, + ids=["(No association)", "(With association)"], +) +@pytest.mark.parametrize( + "test_case", + [ + { + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + }, + ], + ids=["Associate One group with one container registry"], +) +async def test_associate_container_registry_with_group( + extra_fixtures, client: Client, test_case, database_fixture, create_app_and_client +): + test_app, _ = await create_app_and_client( + [ + database_ctx, + ], + [], + ) + + root_ctx = test_app["_root.context"] + context = get_graphquery_context(root_ctx.db) + + query = """ + mutation ($group_id: String!, $registry_id: String!) { + associate_container_registry_with_group(group_id: $group_id, registry_id: $registry_id) { + ok + msg + } + } + """ + + variables = { + "group_id": test_case["group_id"], + "registry_id": test_case["registry_id"], + } + + response = await client.execute_async(query, variables=variables, context_value=context) + already_associated = "association_container_registries_groups" in extra_fixtures + + if already_associated: + assert not response["data"]["associate_container_registry_with_group"]["ok"] + else: + assert response["data"]["associate_container_registry_with_group"]["ok"] + assert response["data"]["associate_container_registry_with_group"]["msg"] == "success" From 25a228a8b0e5312df83d352d6222a847a6dde43e Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Wed, 4 Dec 2024 06:35:13 +0000 Subject: [PATCH 12/21] refactor: Remove common fixture --- .../gql_models/test_container_registries.py | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/manager/models/gql_models/test_container_registries.py b/tests/manager/models/gql_models/test_container_registries.py index 87b1f61ad3..aa0003d77a 100644 --- a/tests/manager/models/gql_models/test_container_registries.py +++ b/tests/manager/models/gql_models/test_container_registries.py @@ -63,28 +63,7 @@ def get_graphquery_context(database_engine: ExtendedAsyncSAEngine) -> GraphQuery FIXTURES_WITH_ASSOC = [ { - "groups": [ - { - "id": "00000000-0000-0000-0000-000000000001", - "name": "mock_group", - "description": "", - "is_active": True, - "domain_name": "default", - "resource_policy": "default", - "total_resource_slots": {}, - "allowed_vfolder_hosts": {}, - "type": "general", - } - ], - "container_registries": [ - { - "id": "00000000-0000-0000-0000-000000000002", - "url": "https://mock.registry.com", - "type": "docker", - "project": "mock_project", - "registry_name": "mock_registry", - } - ], + **fixture, "association_container_registries_groups": [ { "id": "00000000-0000-0000-0000-000000000000", @@ -93,6 +72,7 @@ def get_graphquery_context(database_engine: ExtendedAsyncSAEngine) -> GraphQuery } ], } + for fixture in FIXTURES_WITH_NOASSOC ] From 1f262aabaf64a5b9d38fa3e3b4f7420fbb95a9fd Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Wed, 4 Dec 2024 06:40:06 +0000 Subject: [PATCH 13/21] feat: Add `test_disassociate_container_registry_with_group` --- .../gql_models/test_container_registries.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/manager/models/gql_models/test_container_registries.py b/tests/manager/models/gql_models/test_container_registries.py index aa0003d77a..3add90f49e 100644 --- a/tests/manager/models/gql_models/test_container_registries.py +++ b/tests/manager/models/gql_models/test_container_registries.py @@ -128,3 +128,57 @@ async def test_associate_container_registry_with_group( else: assert response["data"]["associate_container_registry_with_group"]["ok"] assert response["data"]["associate_container_registry_with_group"]["msg"] == "success" + + +@pytest.mark.dependency() +@pytest.mark.asyncio +@pytest.mark.parametrize( + "extra_fixtures", + FIXTURES_WITH_ASSOC + FIXTURES_WITH_NOASSOC, + ids=["(With association)", "(No association)"], +) +@pytest.mark.parametrize( + "test_case", + [ + { + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + }, + ], + ids=["Disassociate One group with one container registry"], +) +async def test_disassociate_container_registry_with_group( + extra_fixtures, client: Client, test_case, database_fixture, create_app_and_client +): + test_app, _ = await create_app_and_client( + [ + database_ctx, + ], + [], + ) + + root_ctx = test_app["_root.context"] + context = get_graphquery_context(root_ctx.db) + + query = """ + mutation ($group_id: String!, $registry_id: String!) { + disassociate_container_registry_with_group(group_id: $group_id, registry_id: $registry_id) { + ok + msg + } + } + """ + + variables = { + "group_id": test_case["group_id"], + "registry_id": test_case["registry_id"], + } + + response = await client.execute_async(query, variables=variables, context_value=context) + association_exist = "association_container_registries_groups" in extra_fixtures + + if association_exist: + assert response["data"]["disassociate_container_registry_with_group"]["ok"] + assert response["data"]["disassociate_container_registry_with_group"]["msg"] == "success" + else: + assert not response["data"]["disassociate_container_registry_with_group"]["ok"] From 0a1da0e9f978651de4dd45394e07e6bfc6a8bcd5 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Fri, 6 Dec 2024 08:09:26 +0000 Subject: [PATCH 14/21] chore: Reorder argument --- tests/manager/models/gql_models/test_container_registries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/manager/models/gql_models/test_container_registries.py b/tests/manager/models/gql_models/test_container_registries.py index 3add90f49e..778cace373 100644 --- a/tests/manager/models/gql_models/test_container_registries.py +++ b/tests/manager/models/gql_models/test_container_registries.py @@ -94,7 +94,7 @@ def get_graphquery_context(database_engine: ExtendedAsyncSAEngine) -> GraphQuery ids=["Associate One group with one container registry"], ) async def test_associate_container_registry_with_group( - extra_fixtures, client: Client, test_case, database_fixture, create_app_and_client + client: Client, database_fixture, extra_fixtures, test_case, create_app_and_client ): test_app, _ = await create_app_and_client( [ @@ -148,7 +148,7 @@ async def test_associate_container_registry_with_group( ids=["Disassociate One group with one container registry"], ) async def test_disassociate_container_registry_with_group( - extra_fixtures, client: Client, test_case, database_fixture, create_app_and_client + client: Client, database_fixture, extra_fixtures, test_case, create_app_and_client ): test_app, _ = await create_app_and_client( [ From c7305e7508b46bcd90ae0d8d6759a62c6d27cb99 Mon Sep 17 00:00:00 2001 From: Gyubong Lee Date: Tue, 10 Dec 2024 01:46:00 +0000 Subject: [PATCH 15/21] feat: Add REST API tests and improve exception handling --- .../backend/manager/api/container_registry.py | 12 +- src/ai/backend/manager/api/exceptions.py | 4 + .../manager/api/test_container_registries.py | 158 ++++++++++++++++++ tests/manager/conftest.py | 1 + 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 tests/manager/api/test_container_registries.py diff --git a/src/ai/backend/manager/api/container_registry.py b/src/ai/backend/manager/api/container_registry.py index 0e6fac9bea..c28d4c575f 100644 --- a/src/ai/backend/manager/api/container_registry.py +++ b/src/ai/backend/manager/api/container_registry.py @@ -7,6 +7,7 @@ import sqlalchemy as sa import trafaret as t from aiohttp import web +from sqlalchemy.exc import IntegrityError from ai.backend.common import validators as tx from ai.backend.logging import BraceStyleAdapter @@ -14,6 +15,8 @@ AssociationContainerRegistriesGroupsRow, ) +from .exceptions import ContainerRegistryNotFound, GenericBadRequest + if TYPE_CHECKING: from .context import RootContext @@ -45,7 +48,10 @@ async def associate_with_group(request: web.Request, params: Any) -> web.Respons "group_id": group_id, }) - await db_sess.execute(insert_query) + try: + await db_sess.execute(insert_query) + except IntegrityError: + raise GenericBadRequest("Association already exists.") return web.json_response({}) @@ -71,7 +77,9 @@ async def disassociate_with_group(request: web.Request, params: Any) -> web.Resp .where(AssociationContainerRegistriesGroupsRow.group_id == group_id) ) - await db_sess.execute(delete_query) + result = await db_sess.execute(delete_query) + if result.rowcount == 0: + raise ContainerRegistryNotFound() return web.json_response({}) diff --git a/src/ai/backend/manager/api/exceptions.py b/src/ai/backend/manager/api/exceptions.py index 23f381d30b..8db4ee7db9 100644 --- a/src/ai/backend/manager/api/exceptions.py +++ b/src/ai/backend/manager/api/exceptions.py @@ -242,6 +242,10 @@ class EndpointTokenNotFound(ObjectNotFound): object_name = "endpoint_token" +class ContainerRegistryNotFound(ObjectNotFound): + object_name = "container_registry" + + class TooManySessionsMatched(BackendError, web.HTTPNotFound): error_type = "https://api.backend.ai/probs/too-many-sessions-matched" error_title = "Too many sessions matched." diff --git a/tests/manager/api/test_container_registries.py b/tests/manager/api/test_container_registries.py new file mode 100644 index 0000000000..d7f7ef80b0 --- /dev/null +++ b/tests/manager/api/test_container_registries.py @@ -0,0 +1,158 @@ +import json + +import pytest + +from ai.backend.manager.server import ( + database_ctx, + hook_plugin_ctx, + monitoring_ctx, + redis_ctx, + shared_config_ctx, +) + +FIXTURES_WITH_NOASSOC = [ + { + "groups": [ + { + "id": "00000000-0000-0000-0000-000000000001", + "name": "mock_group", + "description": "", + "is_active": True, + "domain_name": "default", + "resource_policy": "default", + "total_resource_slots": {}, + "allowed_vfolder_hosts": {}, + "type": "general", + } + ], + "container_registries": [ + { + "id": "00000000-0000-0000-0000-000000000002", + "url": "https://mock.registry.com", + "type": "docker", + "project": "mock_project", + "registry_name": "mock_registry", + } + ], + } +] + +FIXTURES_WITH_ASSOC = [ + { + **fixture, + "association_container_registries_groups": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + } + ], + } + for fixture in FIXTURES_WITH_NOASSOC +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "extra_fixtures", + FIXTURES_WITH_NOASSOC + FIXTURES_WITH_ASSOC, + ids=["(No association)", "(With association)"], +) +@pytest.mark.parametrize( + "test_case", + [ + { + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + }, + ], + ids=["Associate One group with one container registry"], +) +async def test_associate_container_registry_with_group( + test_case, + etcd_fixture, + extra_fixtures, + database_fixture, + create_app_and_client, + get_headers, +): + app, client = await create_app_and_client( + [ + shared_config_ctx, + database_ctx, + monitoring_ctx, + hook_plugin_ctx, + redis_ctx, + ], + [".container_registry", ".auth"], + ) + + group_id = test_case["group_id"] + registry_id = test_case["registry_id"] + + url = "/container-registries/associate-with-group" + params = {"group_id": group_id, "registry_id": registry_id} + + req_bytes = json.dumps(params).encode() + headers = get_headers("POST", url, req_bytes) + + resp = await client.post(url, data=req_bytes, headers=headers) + association_exist = "association_container_registries_groups" in extra_fixtures + + if association_exist: + assert resp.status == 400 + else: + assert resp.status == 200 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "extra_fixtures", + FIXTURES_WITH_ASSOC + FIXTURES_WITH_NOASSOC, + ids=["(With association)", "(No association)"], +) +@pytest.mark.parametrize( + "test_case", + [ + { + "group_id": "00000000-0000-0000-0000-000000000001", + "registry_id": "00000000-0000-0000-0000-000000000002", + }, + ], + ids=["Disassociate One group with one container registry"], +) +async def test_disassociate_container_registry_with_group( + test_case, + etcd_fixture, + extra_fixtures, + database_fixture, + create_app_and_client, + get_headers, +): + app, client = await create_app_and_client( + [ + shared_config_ctx, + database_ctx, + monitoring_ctx, + hook_plugin_ctx, + redis_ctx, + ], + [".container_registry", ".auth"], + ) + + group_id = test_case["group_id"] + registry_id = test_case["registry_id"] + + url = "/container-registries/disassociate-with-group" + params = {"group_id": group_id, "registry_id": registry_id} + + req_bytes = json.dumps(params).encode() + headers = get_headers("POST", url, req_bytes) + + resp = await client.post(url, data=req_bytes, headers=headers) + association_exist = "association_container_registries_groups" in extra_fixtures + + if association_exist: + assert resp.status == 200 + else: + assert resp.status == 404 diff --git a/tests/manager/conftest.py b/tests/manager/conftest.py index 48e5be316b..d4deff664b 100644 --- a/tests/manager/conftest.py +++ b/tests/manager/conftest.py @@ -255,6 +255,7 @@ def etcd_fixture( cli_ctx._local_config = local_config # override the lazy-loaded config with tempfile.NamedTemporaryFile(mode="w", suffix=".etcd.json") as f: etcd_fixture = { + "manager": {"status": "running"}, "volumes": { "_mount": str(vfolder_mount), "_fsprefix": str(vfolder_fsprefix), From de9d0a3eec1edc908e86585c61ab08dc296e7278 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Mon, 6 Jan 2025 02:21:36 +0000 Subject: [PATCH 16/21] fix: Broken CI --- tests/manager/models/gql_models/test_container_registries.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/manager/models/gql_models/test_container_registries.py b/tests/manager/models/gql_models/test_container_registries.py index 778cace373..900bbd3bb1 100644 --- a/tests/manager/models/gql_models/test_container_registries.py +++ b/tests/manager/models/gql_models/test_container_registries.py @@ -31,6 +31,7 @@ def get_graphquery_context(database_engine: ExtendedAsyncSAEngine) -> GraphQuery storage_manager=None, # type: ignore registry=None, # type: ignore idle_checker_host=None, # type: ignore + network_plugin_ctx=None, # type: ignore ) From b1f2befb82c992f4a7466fb2ff715050d389f88e Mon Sep 17 00:00:00 2001 From: jopemachine Date: Mon, 6 Jan 2025 06:58:30 +0000 Subject: [PATCH 17/21] fix: Return 204 response --- src/ai/backend/manager/api/container_registry.py | 4 ++-- tests/manager/api/test_container_registries.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ai/backend/manager/api/container_registry.py b/src/ai/backend/manager/api/container_registry.py index c28d4c575f..879827d73e 100644 --- a/src/ai/backend/manager/api/container_registry.py +++ b/src/ai/backend/manager/api/container_registry.py @@ -53,7 +53,7 @@ async def associate_with_group(request: web.Request, params: Any) -> web.Respons except IntegrityError: raise GenericBadRequest("Association already exists.") - return web.json_response({}) + return web.Response(status=204) @server_status_required(READ_ALLOWED) @@ -81,7 +81,7 @@ async def disassociate_with_group(request: web.Request, params: Any) -> web.Resp if result.rowcount == 0: raise ContainerRegistryNotFound() - return web.json_response({}) + return web.Response(status=204) def create_app( diff --git a/tests/manager/api/test_container_registries.py b/tests/manager/api/test_container_registries.py index d7f7ef80b0..7e8d194c61 100644 --- a/tests/manager/api/test_container_registries.py +++ b/tests/manager/api/test_container_registries.py @@ -102,7 +102,7 @@ async def test_associate_container_registry_with_group( if association_exist: assert resp.status == 400 else: - assert resp.status == 200 + assert resp.status == 204 @pytest.mark.asyncio @@ -153,6 +153,6 @@ async def test_disassociate_container_registry_with_group( association_exist = "association_container_registries_groups" in extra_fixtures if association_exist: - assert resp.status == 200 + assert resp.status == 204 else: assert resp.status == 404 From 0c23c5f041c366dff14534623f789ed251c5bb88 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Mon, 6 Jan 2025 07:14:19 +0000 Subject: [PATCH 18/21] refactor: Change API param checker to pydantic --- .../backend/manager/api/container_registry.py | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/ai/backend/manager/api/container_registry.py b/src/ai/backend/manager/api/container_registry.py index 879827d73e..ab0c282b1c 100644 --- a/src/ai/backend/manager/api/container_registry.py +++ b/src/ai/backend/manager/api/container_registry.py @@ -1,15 +1,14 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any, Iterable, Tuple +from typing import TYPE_CHECKING, Iterable, Tuple import aiohttp_cors import sqlalchemy as sa -import trafaret as t from aiohttp import web +from pydantic import AliasChoices, BaseModel, Field from sqlalchemy.exc import IntegrityError -from ai.backend.common import validators as tx from ai.backend.logging import BraceStyleAdapter from ai.backend.manager.models.association_container_registries_groups import ( AssociationContainerRegistriesGroupsRow, @@ -23,24 +22,32 @@ from .auth import superadmin_required from .manager import READ_ALLOWED, server_status_required from .types import CORSOptions, WebMiddleware -from .utils import check_api_params +from .utils import pydantic_params_api_handler log = BraceStyleAdapter(logging.getLogger(__spec__.name)) +class AssociationRequestModel(BaseModel): + registry_id: str = Field( + validation_alias=AliasChoices("registry_id", "registry"), + description="Container registry row's ID", + ) + group_id: str = Field( + validation_alias=AliasChoices("group_id", "group"), + description="Group row's ID", + ) + + @server_status_required(READ_ALLOWED) @superadmin_required -@check_api_params( - t.Dict({ - tx.AliasedKey(["registry_id", "registry"]): t.String, - tx.AliasedKey(["group_id", "group"]): t.String, - }) -) -async def associate_with_group(request: web.Request, params: Any) -> web.Response: - log.info("ASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params["registry_id"], params["group_id"]) +@pydantic_params_api_handler(AssociationRequestModel) +async def associate_with_group( + request: web.Request, params: AssociationRequestModel +) -> web.Response: + log.info("ASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params.registry_id, params.group_id) root_ctx: RootContext = request.app["_root.context"] - registry_id = params["registry_id"] - group_id = params["group_id"] + registry_id = params.registry_id + group_id = params.group_id async with root_ctx.db.begin_session() as db_sess: insert_query = sa.insert(AssociationContainerRegistriesGroupsRow).values({ @@ -56,19 +63,27 @@ async def associate_with_group(request: web.Request, params: Any) -> web.Respons return web.Response(status=204) +class DisassociationRequestModel(BaseModel): + registry_id: str = Field( + validation_alias=AliasChoices("registry_id", "registry"), + description="Container registry row's ID", + ) + group_id: str = Field( + validation_alias=AliasChoices("group_id", "group"), + description="Group row's ID", + ) + + @server_status_required(READ_ALLOWED) @superadmin_required -@check_api_params( - t.Dict({ - tx.AliasedKey(["registry_id", "registry"]): t.String, - tx.AliasedKey(["group_id", "group"]): t.String, - }) -) -async def disassociate_with_group(request: web.Request, params: Any) -> web.Response: - log.info("DISASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params["registry_id"], params["group_id"]) +@pydantic_params_api_handler(DisassociationRequestModel) +async def disassociate_with_group( + request: web.Request, params: DisassociationRequestModel +) -> web.Response: + log.info("DISASSOCIATE_WITH_GROUP (cr:{}, gr:{})", params.registry_id, params.group_id) root_ctx: RootContext = request.app["_root.context"] - registry_id = params["registry_id"] - group_id = params["group_id"] + registry_id = params.registry_id + group_id = params.group_id async with root_ctx.db.begin_session() as db_sess: delete_query = ( From 135da683f1a9e84f3faf3e6f1ed5a1cffd3ef753 Mon Sep 17 00:00:00 2001 From: jopemachine Date: Wed, 8 Jan 2025 03:39:01 +0000 Subject: [PATCH 19/21] fix: Update milestone --- src/ai/backend/manager/api/schema.graphql | 8 ++++---- src/ai/backend/manager/models/gql.py | 4 ++-- .../manager/models/gql_models/container_registry.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index 8c44257d9a..c4bcd9378c 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1849,10 +1849,10 @@ type Mutations { id: String! ): DeleteContainerRegistryNode - """Added in 24.12.0""" + """Added in 25.01.0.""" associate_container_registry_with_group(group_id: String!, registry_id: String!): AssociateContainerRegistryWithGroup - """Added in 24.12.0""" + """Added in 25.01.0.""" disassociate_container_registry_with_group(group_id: String!, registry_id: String!): DisassociateContainerRegistryWithGroup create_container_registry(hostname: String!, props: CreateContainerRegistryInput!): CreateContainerRegistry modify_container_registry(hostname: String!, props: ModifyContainerRegistryInput!): ModifyContainerRegistry @@ -2587,13 +2587,13 @@ type DeleteContainerRegistryNode { container_registry: ContainerRegistryNode } -"""Added in 24.12.0.""" +"""Added in 25.01.0.""" type AssociateContainerRegistryWithGroup { ok: Boolean msg: String } -"""Added in 24.12.0.""" +"""Added in 25.01.0.""" type DisassociateContainerRegistryWithGroup { ok: Boolean msg: String diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index ff6c512d85..fd51399686 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -340,10 +340,10 @@ class Mutations(graphene.ObjectType): ) associate_container_registry_with_group = AssociateContainerRegistryWithGroup.Field( - description="Added in 24.12.0" + description="Added in 25.01.0." ) disassociate_container_registry_with_group = DisassociateContainerRegistryWithGroup.Field( - description="Added in 24.12.0" + description="Added in 25.01.0." ) # Legacy mutations diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py index ae44bef0e2..f0f13ee1de 100644 --- a/src/ai/backend/manager/models/gql_models/container_registry.py +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -18,7 +18,7 @@ class AssociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 24.12.0.""" + """Added in 25.01.0.""" allowed_roles = (UserRole.SUPERADMIN,) @@ -45,7 +45,7 @@ async def mutate( class DisassociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 24.12.0.""" + """Added in 25.01.0.""" allowed_roles = (UserRole.SUPERADMIN,) From 978b571c77b3601bd37e5be3e8aa93fb9baac59c Mon Sep 17 00:00:00 2001 From: jopemachine Date: Wed, 8 Jan 2025 08:35:21 +0000 Subject: [PATCH 20/21] fix: Update milestone --- src/ai/backend/manager/api/schema.graphql | 8 ++++---- src/ai/backend/manager/models/gql.py | 4 ++-- .../manager/models/gql_models/container_registry.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index c4bcd9378c..3e29ad9431 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1849,10 +1849,10 @@ type Mutations { id: String! ): DeleteContainerRegistryNode - """Added in 25.01.0.""" + """Added in 25.1.0.""" associate_container_registry_with_group(group_id: String!, registry_id: String!): AssociateContainerRegistryWithGroup - """Added in 25.01.0.""" + """Added in 25.1.0.""" disassociate_container_registry_with_group(group_id: String!, registry_id: String!): DisassociateContainerRegistryWithGroup create_container_registry(hostname: String!, props: CreateContainerRegistryInput!): CreateContainerRegistry modify_container_registry(hostname: String!, props: ModifyContainerRegistryInput!): ModifyContainerRegistry @@ -2587,13 +2587,13 @@ type DeleteContainerRegistryNode { container_registry: ContainerRegistryNode } -"""Added in 25.01.0.""" +"""Added in 25.1.0.""" type AssociateContainerRegistryWithGroup { ok: Boolean msg: String } -"""Added in 25.01.0.""" +"""Added in 25.1.0.""" type DisassociateContainerRegistryWithGroup { ok: Boolean msg: String diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index fd51399686..6479e7458e 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -340,10 +340,10 @@ class Mutations(graphene.ObjectType): ) associate_container_registry_with_group = AssociateContainerRegistryWithGroup.Field( - description="Added in 25.01.0." + description="Added in 25.1.0." ) disassociate_container_registry_with_group = DisassociateContainerRegistryWithGroup.Field( - description="Added in 25.01.0." + description="Added in 25.1.0." ) # Legacy mutations diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py index f0f13ee1de..97b9f37797 100644 --- a/src/ai/backend/manager/models/gql_models/container_registry.py +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -18,7 +18,7 @@ class AssociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 25.01.0.""" + """Added in 25.1.0.""" allowed_roles = (UserRole.SUPERADMIN,) @@ -45,7 +45,7 @@ async def mutate( class DisassociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 25.01.0.""" + """Added in 25.1.0.""" allowed_roles = (UserRole.SUPERADMIN,) From da74895358ed2c5b7aeaabbe748051f7a9a9ce7c Mon Sep 17 00:00:00 2001 From: jopemachine Date: Tue, 14 Jan 2025 06:16:24 +0000 Subject: [PATCH 21/21] docs: Update milestone --- src/ai/backend/manager/api/schema.graphql | 8 ++++---- src/ai/backend/manager/models/gql.py | 4 ++-- .../manager/models/gql_models/container_registry.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ai/backend/manager/api/schema.graphql b/src/ai/backend/manager/api/schema.graphql index 3e29ad9431..9259bd767d 100644 --- a/src/ai/backend/manager/api/schema.graphql +++ b/src/ai/backend/manager/api/schema.graphql @@ -1849,10 +1849,10 @@ type Mutations { id: String! ): DeleteContainerRegistryNode - """Added in 25.1.0.""" + """Added in 25.2.0.""" associate_container_registry_with_group(group_id: String!, registry_id: String!): AssociateContainerRegistryWithGroup - """Added in 25.1.0.""" + """Added in 25.2.0.""" disassociate_container_registry_with_group(group_id: String!, registry_id: String!): DisassociateContainerRegistryWithGroup create_container_registry(hostname: String!, props: CreateContainerRegistryInput!): CreateContainerRegistry modify_container_registry(hostname: String!, props: ModifyContainerRegistryInput!): ModifyContainerRegistry @@ -2587,13 +2587,13 @@ type DeleteContainerRegistryNode { container_registry: ContainerRegistryNode } -"""Added in 25.1.0.""" +"""Added in 25.2.0.""" type AssociateContainerRegistryWithGroup { ok: Boolean msg: String } -"""Added in 25.1.0.""" +"""Added in 25.2.0.""" type DisassociateContainerRegistryWithGroup { ok: Boolean msg: String diff --git a/src/ai/backend/manager/models/gql.py b/src/ai/backend/manager/models/gql.py index 6479e7458e..7d7332a517 100644 --- a/src/ai/backend/manager/models/gql.py +++ b/src/ai/backend/manager/models/gql.py @@ -340,10 +340,10 @@ class Mutations(graphene.ObjectType): ) associate_container_registry_with_group = AssociateContainerRegistryWithGroup.Field( - description="Added in 25.1.0." + description="Added in 25.2.0." ) disassociate_container_registry_with_group = DisassociateContainerRegistryWithGroup.Field( - description="Added in 25.1.0." + description="Added in 25.2.0." ) # Legacy mutations diff --git a/src/ai/backend/manager/models/gql_models/container_registry.py b/src/ai/backend/manager/models/gql_models/container_registry.py index 97b9f37797..8f99525791 100644 --- a/src/ai/backend/manager/models/gql_models/container_registry.py +++ b/src/ai/backend/manager/models/gql_models/container_registry.py @@ -18,7 +18,7 @@ class AssociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 25.1.0.""" + """Added in 25.2.0.""" allowed_roles = (UserRole.SUPERADMIN,) @@ -45,7 +45,7 @@ async def mutate( class DisassociateContainerRegistryWithGroup(graphene.Mutation): - """Added in 25.1.0.""" + """Added in 25.2.0.""" allowed_roles = (UserRole.SUPERADMIN,)