diff --git a/changes/11312.feature.md b/changes/11312.feature.md new file mode 100644 index 00000000000..74faa4053dc --- /dev/null +++ b/changes/11312.feature.md @@ -0,0 +1 @@ +Add `AppConfigPolicy` REST v2 surface (`POST /v2/app-config-policies/search`, `GET /v2/app-config-policies/{policy_id}`, admin `bulk-create` / `bulk-update` / `bulk-purge`) — pairs with the GraphQL surface from BA-5815. diff --git a/src/ai/backend/manager/api/rest/v2/app_config_policy/__init__.py b/src/ai/backend/manager/api/rest/v2/app_config_policy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/ai/backend/manager/api/rest/v2/app_config_policy/handler.py b/src/ai/backend/manager/api/rest/v2/app_config_policy/handler.py new file mode 100644 index 00000000000..81e0fb5a402 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config_policy/handler.py @@ -0,0 +1,78 @@ +"""REST v2 handler for the app-config policy domain. + +Writes are **bulk-only** — the single-item create / update / purge +endpoints were removed in favour of `/bulk-create`, `/bulk-update`, +`/bulk-purge` (admin-only). +""" + +from __future__ import annotations + +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Final + +from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam +from ai.backend.common.dto.manager.v2.app_config_policy.request import ( + AdminBulkCreateAppConfigPoliciesInput, + AdminBulkPurgeAppConfigPoliciesInput, + AdminBulkUpdateAppConfigPoliciesInput, + SearchAppConfigPoliciesInput, +) +from ai.backend.logging import BraceStyleAdapter +from ai.backend.manager.api.rest.v2.path_params import AppConfigPolicyIdPathParam + +if TYPE_CHECKING: + from ai.backend.manager.api.adapters.app_config_policy import AppConfigPolicyAdapter + +log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name)) + + +class V2AppConfigPolicyHandler: + """REST v2 handler for app-config policy operations.""" + + def __init__(self, *, adapter: AppConfigPolicyAdapter) -> None: + self._adapter = adapter + + # ── Reads ──────────────────────────────────────────────────── + + async def get( + self, + path: PathParam[AppConfigPolicyIdPathParam], + ) -> APIResponse: + """Read a single policy by row id (any authenticated user).""" + result = await self._adapter.get(path.parsed.policy_id) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def search( + self, + body: BodyParam[SearchAppConfigPoliciesInput], + ) -> APIResponse: + """Paginated policy search (any authenticated user).""" + result = await self._adapter.search(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + # ── Admin bulk writes ──────────────────────────────────────── + + async def admin_bulk_create( + self, + body: BodyParam[AdminBulkCreateAppConfigPoliciesInput], + ) -> APIResponse: + """Strict insert; per-item transactions (admin only).""" + result = await self._adapter.admin_bulk_create(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_bulk_update( + self, + body: BodyParam[AdminBulkUpdateAppConfigPoliciesInput], + ) -> APIResponse: + """Replace `scope_sources` (admin only). `config_name` is immutable.""" + result = await self._adapter.admin_bulk_update(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) + + async def admin_bulk_purge( + self, + body: BodyParam[AdminBulkPurgeAppConfigPoliciesInput], + ) -> APIResponse: + """Hard-delete by row id (admin only); rows still referenced by fragments fail per-item.""" + result = await self._adapter.admin_bulk_purge(body.parsed) + return APIResponse.build(status_code=HTTPStatus.OK, response_model=result) diff --git a/src/ai/backend/manager/api/rest/v2/app_config_policy/registry.py b/src/ai/backend/manager/api/rest/v2/app_config_policy/registry.py new file mode 100644 index 00000000000..13fc8409a07 --- /dev/null +++ b/src/ai/backend/manager/api/rest/v2/app_config_policy/registry.py @@ -0,0 +1,36 @@ +"""Route registration for v2 app-config policy endpoints.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ai.backend.manager.api.rest.middleware.auth import auth_required, superadmin_required +from ai.backend.manager.api.rest.routing import RouteRegistry + +from .handler import V2AppConfigPolicyHandler + +if TYPE_CHECKING: + from ai.backend.manager.api.rest.types import RouteDeps + + +def register_v2_app_config_policy_routes( + handler: V2AppConfigPolicyHandler, + route_deps: RouteDeps, +) -> RouteRegistry: + """Register all v2 app-config policy routes. + + Reads (`GET /{policy_id}`, `POST /search`) are available to any + authenticated user. Writes are bulk-only and admin-only — + `/bulk-create`, `/bulk-update`, `/bulk-purge`. + """ + reg = RouteRegistry.create("app-config-policies", route_deps.cors_options) + + # Reads + reg.add("POST", "/search", handler.search, middlewares=[auth_required]) + reg.add("GET", "/{policy_id}", handler.get, middlewares=[auth_required]) + # Admin bulk writes + reg.add("POST", "/bulk-create", handler.admin_bulk_create, middlewares=[superadmin_required]) + reg.add("POST", "/bulk-update", handler.admin_bulk_update, middlewares=[superadmin_required]) + reg.add("POST", "/bulk-purge", handler.admin_bulk_purge, middlewares=[superadmin_required]) + + return reg diff --git a/src/ai/backend/manager/api/rest/v2/path_params.py b/src/ai/backend/manager/api/rest/v2/path_params.py index 88cfc7b59cf..1b3ccbaff75 100644 --- a/src/ai/backend/manager/api/rest/v2/path_params.py +++ b/src/ai/backend/manager/api/rest/v2/path_params.py @@ -9,6 +9,10 @@ from ai.backend.common.api_handlers import BaseRequestModel +class AppConfigPolicyIdPathParam(BaseRequestModel): + policy_id: UUID = Field(description="App-config policy row UUID") + + class DomainNamePathParam(BaseRequestModel): domain_name: str = Field(description="Domain name") diff --git a/src/ai/backend/manager/api/rest/v2/tree.py b/src/ai/backend/manager/api/rest/v2/tree.py index 34bc31b8744..b794551f77f 100644 --- a/src/ai/backend/manager/api/rest/v2/tree.py +++ b/src/ai/backend/manager/api/rest/v2/tree.py @@ -28,6 +28,8 @@ def build_v2_routes( # Lazy imports to avoid circular dependencies at module level from .agent.handler import V2AgentHandler from .agent.registry import register_v2_agent_routes + from .app_config_policy.handler import V2AppConfigPolicyHandler + from .app_config_policy.registry import register_v2_app_config_policy_routes from .artifact.handler import V2ArtifactHandler from .artifact.registry import register_v2_artifact_routes from .artifact_registry.handler import V2ArtifactRegistryHandler @@ -115,6 +117,7 @@ def build_v2_routes( # Build all handlers (each takes its individual adapter) agent_handler = V2AgentHandler(adapter=adapters.agent) + app_config_policy_handler = V2AppConfigPolicyHandler(adapter=adapters.app_config_policy) artifact_handler = V2ArtifactHandler(adapter=adapters.artifact) artifact_registry_handler = V2ArtifactRegistryHandler(adapter=adapters.artifact_registry) audit_log_handler = V2AuditLogHandler(adapter=adapters.audit_log) @@ -171,6 +174,9 @@ def build_v2_routes( # Add all domain sub-registries v2_reg.add_subregistry(register_v2_agent_routes(agent_handler, route_deps)) + v2_reg.add_subregistry( + register_v2_app_config_policy_routes(app_config_policy_handler, route_deps) + ) v2_reg.add_subregistry(register_v2_artifact_routes(artifact_handler, route_deps)) v2_reg.add_subregistry( register_v2_artifact_registry_routes(artifact_registry_handler, route_deps)