Skip to content

Commit 4b52adf

Browse files
jopemachineclaude
andcommitted
feat(BA-5830): add /v2/app-configs REST endpoints (merged view, BEP-1052 §5)
Mounts the merged-view `AppConfig` surface as REST v2 under `/v2/app-configs/...`. Shares v2 DTOs and the adapter with the GraphQL layer added in BA-5829 (#11285). Handler reuses the Fragment adapter because the merged-view methods live on `AppConfigFragmentAdapter`. Endpoints: - `GET /v2/app-configs/my/{name}` — auth (self-service single) - `POST /v2/app-configs/my/search` — auth (self-service paginated) - `POST /v2/app-configs/search` — superadmin (cross-user) - `GET /v2/app-configs/{user_id}/{name}` — superadmin (read one user's view) Adds: - `api/rest/v2/app_config/handler.V2AppConfigHandler` with `my_get` / `my_search` / `admin_get` / `admin_search`. - `api/rest/v2/app_config/registry.register_v2_app_config_routes`. - `AppConfigMyNamePathParam` + `AppConfigUserNamePathParam` in the shared path-params module. - Handler instantiation + sub-registry wiring in `rest/v2/tree.py` (handler takes `adapters.app_config_fragment` — annotated for the reviewer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3524cec commit 4b52adf

5 files changed

Lines changed: 125 additions & 0 deletions

File tree

src/ai/backend/manager/api/rest/v2/app_config/__init__.py

Whitespace-only changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""REST v2 handler for the AppConfig merged-view domain (BEP-1052 §5)."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from http import HTTPStatus
7+
from typing import TYPE_CHECKING, Final
8+
9+
from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam
10+
from ai.backend.common.dto.manager.v2.app_config.request import (
11+
GetUserAppConfigInput,
12+
SearchAppConfigsInput,
13+
SearchMyAppConfigsInput,
14+
)
15+
from ai.backend.logging import BraceStyleAdapter
16+
from ai.backend.manager.api.rest.v2.path_params import (
17+
AppConfigMyNamePathParam,
18+
AppConfigUserNamePathParam,
19+
)
20+
21+
if TYPE_CHECKING:
22+
from ai.backend.manager.api.adapters.app_config_fragment import AppConfigFragmentAdapter
23+
24+
log: Final = BraceStyleAdapter(logging.getLogger(__spec__.name))
25+
26+
27+
class V2AppConfigHandler:
28+
"""REST v2 handler for the merged-view `AppConfig` surface.
29+
30+
Mounted at `/v2/app-configs/...`. The adapter lives on the Fragment
31+
domain (`app_config_fragment`) because the merged view is computed
32+
from fragment rows joined against `app_config_policies`.
33+
"""
34+
35+
def __init__(self, *, adapter: AppConfigFragmentAdapter) -> None:
36+
self._adapter = adapter
37+
38+
# ── My (self-service) ────────────────────────────────────────
39+
40+
async def my_get(
41+
self,
42+
path: PathParam[AppConfigMyNamePathParam],
43+
) -> APIResponse:
44+
"""Read the caller's own merged AppConfig for `name`."""
45+
result = await self._adapter.my_app_config(path.parsed.name)
46+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
47+
48+
async def my_search(
49+
self,
50+
body: BodyParam[SearchMyAppConfigsInput],
51+
) -> APIResponse:
52+
"""Paginated merged-view search over the caller's own AppConfigs."""
53+
result = await self._adapter.my_search_app_configs(body.parsed)
54+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
55+
56+
# ── Admin ────────────────────────────────────────────────────
57+
58+
async def admin_get(
59+
self,
60+
path: PathParam[AppConfigUserNamePathParam],
61+
) -> APIResponse:
62+
"""Read a specific user's merged AppConfig (admin only)."""
63+
result = await self._adapter.admin_get_user_app_config(
64+
GetUserAppConfigInput(user_id=path.parsed.user_id, name=path.parsed.name)
65+
)
66+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
67+
68+
async def admin_search(
69+
self,
70+
body: BodyParam[SearchAppConfigsInput],
71+
) -> APIResponse:
72+
"""Cross-user merged-view search (admin only)."""
73+
result = await self._adapter.admin_search_app_configs(body.parsed)
74+
return APIResponse.build(status_code=HTTPStatus.OK, response_model=result)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Route registration for v2 AppConfig merged-view endpoints (BEP-1052 §5)."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from ai.backend.manager.api.rest.middleware.auth import auth_required, superadmin_required
8+
from ai.backend.manager.api.rest.routing import RouteRegistry
9+
10+
from .handler import V2AppConfigHandler
11+
12+
if TYPE_CHECKING:
13+
from ai.backend.manager.api.rest.types import RouteDeps
14+
15+
16+
def register_v2_app_config_routes(
17+
handler: V2AppConfigHandler,
18+
route_deps: RouteDeps,
19+
) -> RouteRegistry:
20+
"""Register all v2 `/v2/app-configs/*` routes (BEP-1052 §4).
21+
22+
Read-only surface — writes go through `/v2/app-config-fragments/...`
23+
(§4). Self-service routes land under the `/my/...` prefix so the
24+
adapter can pin `(USER, current_user)` internally; admin routes
25+
allow targeting any user id.
26+
"""
27+
reg = RouteRegistry.create("app-configs", route_deps.cors_options)
28+
29+
# Self-service
30+
reg.add("GET", "/my/{name}", handler.my_get, middlewares=[auth_required])
31+
reg.add("POST", "/my/search", handler.my_search, middlewares=[auth_required])
32+
# Admin
33+
reg.add("POST", "/search", handler.admin_search, middlewares=[superadmin_required])
34+
reg.add("GET", "/{user_id}/{name}", handler.admin_get, middlewares=[superadmin_required])
35+
36+
return reg

src/ai/backend/manager/api/rest/v2/path_params.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ class AppConfigFragmentScopePathParam(BaseRequestModel):
1414
scope_id: str = Field(description="Scope id (domain name, user id, or `public`).")
1515

1616

17+
class AppConfigMyNamePathParam(BaseRequestModel):
18+
name: str = Field(description="Policy / config name.")
19+
20+
21+
class AppConfigUserNamePathParam(BaseRequestModel):
22+
user_id: UUID = Field(description="Target user's UUID (admin only).")
23+
name: str = Field(description="Policy / config name.")
24+
25+
1726
class DomainNamePathParam(BaseRequestModel):
1827
domain_name: str = Field(description="Domain name")
1928

src/ai/backend/manager/api/rest/v2/tree.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def build_v2_routes(
2828
# Lazy imports to avoid circular dependencies at module level
2929
from .agent.handler import V2AgentHandler
3030
from .agent.registry import register_v2_agent_routes
31+
from .app_config.handler import V2AppConfigHandler
32+
from .app_config.registry import register_v2_app_config_routes
3133
from .app_config_fragment.handler import V2AppConfigFragmentHandler
3234
from .app_config_fragment.registry import register_v2_app_config_fragment_routes
3335
from .artifact.handler import V2ArtifactHandler
@@ -117,6 +119,9 @@ def build_v2_routes(
117119

118120
# Build all handlers (each takes its individual adapter)
119121
agent_handler = V2AgentHandler(adapter=adapters.agent)
122+
# AppConfig merged-view handler reuses the Fragment adapter because the
123+
# merged-view methods live on `AppConfigFragmentAdapter`.
124+
app_config_handler = V2AppConfigHandler(adapter=adapters.app_config_fragment)
120125
app_config_fragment_handler = V2AppConfigFragmentHandler(adapter=adapters.app_config_fragment)
121126
artifact_handler = V2ArtifactHandler(adapter=adapters.artifact)
122127
artifact_registry_handler = V2ArtifactRegistryHandler(adapter=adapters.artifact_registry)
@@ -174,6 +179,7 @@ def build_v2_routes(
174179

175180
# Add all domain sub-registries
176181
v2_reg.add_subregistry(register_v2_agent_routes(agent_handler, route_deps))
182+
v2_reg.add_subregistry(register_v2_app_config_routes(app_config_handler, route_deps))
177183
v2_reg.add_subregistry(
178184
register_v2_app_config_fragment_routes(app_config_fragment_handler, route_deps)
179185
)

0 commit comments

Comments
 (0)