Skip to content

Commit 4b8f701

Browse files
jopemachineclaude
andcommitted
refactor(BA-5979): split deployment search into admin and scoped layers
Restructures the deployment search/projection stack so every layer (db_source → repository → service → processor → adapter → REST/GQL/SDK/CLI) makes the search scope explicit: - Admin-only search: ``DeploymentAdminRepository``/``DeploymentAdminService`` with ``admin_search_deployments`` at every level. The DBSource side carries the prefix too so the unscoped intent is visible alongside the scoped ``search_user_deployments`` / ``search_project_deployments`` variants in the same namespace. - User/project search: dedicated actions (``SearchUserDeploymentsAction``, ``SearchProjectDeploymentsAction``) with their own RBAC scope. The v2 adapter's ``my_search`` / ``project_search`` now route through these. - Project summary: ``SearchProjectDeploymentSummaryAction`` for the lightweight project-admin list view (renamed from ``SearchDeploymentsInProject``). EndpointRow → ModelDeploymentData projection moves onto the row as ``EndpointRow.to_model_deployment_data()``, following the existing ``to_deployment_info`` shape and the BA-6056 relationship split: the projection reads ``current_revision_row`` / ``deploying_revision_row`` directly instead of scanning a ``revisions`` list. The fragile ``DeploymentInfo``-via-helper path (``_convert_deployment_info_to_data``) disappears together with its placeholder defaults; service callers now fetch ``ModelDeploymentData`` straight from the repository. API surface: - v1 REST: ``POST /deployments/search`` contract preserved; a new ``SearchLegacyDeploymentsRequest`` (standalone, not a subclass) carries the ``project_id`` inline on the body, and the handler routes through ``SearchProjectDeploymentsAction``. v1 SDK, ``./bai`` CLI, and ``client/func`` migrate to the new request type. - v2 REST/GQL: ``SearchDeploymentsInput`` / ``SearchDeploymentsPayload`` (neutral names, no admin prefix) are shared by admin / project / my variants — mirrors the vfolder / model_card / user / session v2 convention. Admin and scope variants only differ in URL + middleware. - GraphQL ``deployment_loader`` keeps the unscoped batch lookup it was born with, now routed through ``admin_search_deployments`` rather than the legacy slot — parent resolvers stay responsible for RBAC. Tests: - ``tests/unit/manager/repositories/deployment/test_endpoint_projection.py`` pins the new ``EndpointRow.to_model_deployment_data`` against the ``current_revision_row`` relationship (replaces the BA-5963 list-order regression test, which is structurally impossible under the new lookup). - v1 component / unit / SDK tests migrate to ``SearchLegacyDeploymentsRequest``. - Existing ``SearchProjectDeploymentSummary`` action becomes ``@dataclass(frozen=True)`` to match the rest of the search-action set. Co-Authored-By: Gyubong <gbl@lablup.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 56c4207 commit 4b8f701

45 files changed

Lines changed: 1084 additions & 386 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

changes/11522.enhance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Project ``EndpointRow`` directly to ``ModelDeploymentData`` for the API path so deployment responses no longer pass through a fragile ``DeploymentInfo`` conversion, and split no-scope deployment reads into a dedicated admin repository/service/processor.

src/ai/backend/client/cli/deployment.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,31 +93,28 @@ def create_deployment_cmd(
9393

9494
@deployment.command("list")
9595
@pass_ctx_obj
96-
@click.option("--project-id", type=str, default=None, help="Filter by project ID")
96+
@click.option("--project-id", type=str, required=True, help="Project ID to search within")
9797
@click.option("--limit", type=int, default=50, help="Maximum items to return")
9898
@click.option("--offset", type=int, default=0, help="Number of items to skip")
9999
def list_deployments_cmd(
100100
ctx: CLIContext,
101-
project_id: str | None,
101+
project_id: str,
102102
limit: int,
103103
offset: int,
104104
) -> None:
105-
"""List all deployments."""
105+
"""List deployments within a project."""
106106
from uuid import UUID
107107

108108
from ai.backend.client.session import Session
109-
from ai.backend.common.dto.manager.deployment import (
110-
DeploymentFilter,
111-
SearchDeploymentsRequest,
112-
)
109+
from ai.backend.common.dto.manager.deployment import SearchLegacyDeploymentsRequest
113110

114111
with Session() as session:
115112
try:
116-
filter_cond = None
117-
if project_id:
118-
filter_cond = DeploymentFilter(project_id=UUID(project_id))
119-
120-
request = SearchDeploymentsRequest(filter=filter_cond, limit=limit, offset=offset)
113+
request = SearchLegacyDeploymentsRequest(
114+
project_id=UUID(project_id),
115+
limit=limit,
116+
offset=offset,
117+
)
121118
result = session.Deployment.search(request)
122119

123120
deployments = result.deployments

src/ai/backend/client/cli/v2/admin/deployment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ def search(
6262
"""Search all deployments (admin)."""
6363
from ai.backend.common.dto.manager.query import StringFilter
6464
from ai.backend.common.dto.manager.v2.deployment.request import (
65-
AdminSearchDeploymentsInput,
6665
DeploymentFilter,
6766
DeploymentOrder,
6867
DeploymentStatusFilter,
68+
SearchDeploymentsInput,
6969
)
7070
from ai.backend.common.dto.manager.v2.deployment.types import DeploymentOrderField
7171

@@ -99,7 +99,7 @@ async def _run() -> None:
9999
registry = await create_v2_registry(load_v2_config())
100100
try:
101101
result = await registry.deployment.admin_search(
102-
AdminSearchDeploymentsInput(
102+
SearchDeploymentsInput(
103103
filter=filter_dto,
104104
order=orders,
105105
limit=limit,

src/ai/backend/client/cli/v2/deployment/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ def project_search(
5050

5151
from ai.backend.common.dto.manager.query import StringFilter
5252
from ai.backend.common.dto.manager.v2.deployment.request import (
53-
AdminSearchDeploymentsInput,
5453
DeploymentFilter,
5554
DeploymentOrder,
5655
DeploymentStatusFilter,
56+
SearchDeploymentsInput,
5757
)
5858
from ai.backend.common.dto.manager.v2.deployment.types import DeploymentOrderField
5959

@@ -79,7 +79,7 @@ def project_search(
7979
)
8080
orders = parsed
8181

82-
body = AdminSearchDeploymentsInput(
82+
body = SearchDeploymentsInput(
8383
filter=filter_dto,
8484
order=orders,
8585
limit=limit,

src/ai/backend/client/cli/v2/my/deployment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def search(
4141

4242
from ai.backend.common.dto.manager.query import StringFilter
4343
from ai.backend.common.dto.manager.v2.deployment.request import (
44-
AdminSearchDeploymentsInput,
4544
DeploymentFilter,
4645
DeploymentStatusFilter,
46+
SearchDeploymentsInput,
4747
)
4848

4949
filter_dto: DeploymentFilter | None = None
@@ -56,7 +56,7 @@ def search(
5656
async def _run() -> None:
5757
registry = await create_v2_registry(load_v2_config())
5858
try:
59-
request = AdminSearchDeploymentsInput(
59+
request = SearchDeploymentsInput(
6060
filter=filter_dto,
6161
first=first,
6262
after=after,

src/ai/backend/client/func/deployment.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
ListDeploymentsResponse,
1818
ListRevisionsResponse,
1919
ListRoutesResponse,
20-
SearchDeploymentsRequest,
20+
SearchLegacyDeploymentsRequest,
2121
SearchRevisionsRequest,
2222
SearchRoutesRequest,
2323
UpdateDeploymentRequest,
@@ -58,9 +58,9 @@ async def create(
5858
@classmethod
5959
async def search(
6060
cls,
61-
request: SearchDeploymentsRequest,
61+
request: SearchLegacyDeploymentsRequest,
6262
) -> ListDeploymentsResponse:
63-
"""Search deployments with filters."""
63+
"""Search deployments within a project."""
6464
rqst = Request("POST", "/deployments/search")
6565
rqst.set_json(request.model_dump(mode="json"))
6666
async with rqst.fetch() as resp:

src/ai/backend/client/v2/domains/deployment.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
ListRevisionsResponse,
2020
ListRoutesResponse,
2121
SearchDeploymentPoliciesRequest,
22-
SearchDeploymentsRequest,
22+
SearchLegacyDeploymentsRequest,
2323
SearchRevisionsRequest,
2424
SearchRoutesRequest,
2525
UpdateDeploymentRequest,
@@ -51,7 +51,7 @@ async def create_deployment(
5151

5252
async def search_deployments(
5353
self,
54-
request: SearchDeploymentsRequest,
54+
request: SearchLegacyDeploymentsRequest,
5555
) -> ListDeploymentsResponse:
5656
return await self._client.typed_request(
5757
"POST",

src/ai/backend/client/v2/domains_v2/deployment.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from ai.backend.common.dto.manager.v2.deployment.request import (
1515
ActivateRevisionInput,
1616
AddRevisionInput,
17-
AdminSearchDeploymentsInput,
1817
AdminSearchRevisionsInput,
1918
BulkDeleteAccessTokensInput,
2019
CreateAccessTokenInput,
@@ -25,6 +24,7 @@
2524
SearchAccessTokensInput,
2625
SearchAutoScalingRulesInput,
2726
SearchDeploymentPoliciesInput,
27+
SearchDeploymentsInput,
2828
SearchReplicasInput,
2929
SearchRoutesInput,
3030
SyncReplicaInput,
@@ -36,7 +36,6 @@
3636
ActivateRevisionPayload,
3737
AddRevisionPayload,
3838
AdminRefreshDeploymentRevisionsPayload,
39-
AdminSearchDeploymentsPayload,
4039
AdminSearchRevisionsPayload,
4140
BulkDeleteAccessTokensPayload,
4241
BulkDeleteAutoScalingRulesPayload,
@@ -56,6 +55,7 @@
5655
SearchAccessTokensPayload,
5756
SearchAutoScalingRulesPayload,
5857
SearchDeploymentPoliciesPayload,
58+
SearchDeploymentsPayload,
5959
SearchReplicasPayload,
6060
SearchRoutesPayload,
6161
SyncReplicaPayload,
@@ -91,39 +91,39 @@ async def create(
9191

9292
async def admin_search(
9393
self,
94-
body: AdminSearchDeploymentsInput,
95-
) -> AdminSearchDeploymentsPayload:
94+
body: SearchDeploymentsInput,
95+
) -> SearchDeploymentsPayload:
9696
"""Search deployments with admin scope (superadmin only)."""
9797
return await self._client.typed_request(
9898
"POST",
9999
_PATH + "/search",
100100
request=body,
101-
response_model=AdminSearchDeploymentsPayload,
101+
response_model=SearchDeploymentsPayload,
102102
)
103103

104104
async def my_search(
105105
self,
106-
body: AdminSearchDeploymentsInput,
107-
) -> AdminSearchDeploymentsPayload:
106+
body: SearchDeploymentsInput,
107+
) -> SearchDeploymentsPayload:
108108
"""Search deployments owned by the current user."""
109109
return await self._client.typed_request(
110110
"POST",
111111
f"{_PATH}/my/search",
112112
request=body,
113-
response_model=AdminSearchDeploymentsPayload,
113+
response_model=SearchDeploymentsPayload,
114114
)
115115

116116
async def project_search(
117117
self,
118118
project_id: UUID,
119-
body: AdminSearchDeploymentsInput,
120-
) -> AdminSearchDeploymentsPayload:
119+
body: SearchDeploymentsInput,
120+
) -> SearchDeploymentsPayload:
121121
"""Search deployments within a specific project."""
122122
return await self._client.typed_request(
123123
"POST",
124124
f"{_PATH}/projects/{project_id}/search",
125125
request=body,
126-
response_model=AdminSearchDeploymentsPayload,
126+
response_model=SearchDeploymentsPayload,
127127
)
128128

129129
async def get(

src/ai/backend/common/data/model_deployment/types.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any, Self
22

3+
from ai.backend.common.data.endpoint.types import EndpointLifecycle
34
from ai.backend.common.types import CIStrEnum
45

56

@@ -48,6 +49,20 @@ def _missing_(cls, value: Any) -> Self | None:
4849
return cls(alias)
4950
return super()._missing_(value)
5051

52+
@classmethod
53+
def from_lifecycle(cls, lifecycle: EndpointLifecycle) -> "ModelDeploymentStatus":
54+
match lifecycle:
55+
case EndpointLifecycle.PENDING | EndpointLifecycle.CREATED:
56+
return cls.PENDING
57+
case EndpointLifecycle.READY | EndpointLifecycle.SCALING:
58+
return cls.READY
59+
case EndpointLifecycle.DEPLOYING:
60+
return cls.DEPLOYING
61+
case EndpointLifecycle.DESTROYING:
62+
return cls.STOPPING
63+
case EndpointLifecycle.DESTROYED:
64+
return cls.STOPPED
65+
5166

5267
class DeploymentStrategy(CIStrEnum):
5368
ROLLING = "ROLLING"

src/ai/backend/common/dto/manager/deployment/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
RouteFilter,
2929
RoutePathParam,
3030
SearchDeploymentPoliciesRequest,
31-
SearchDeploymentsRequest,
31+
SearchLegacyDeploymentsRequest,
3232
SearchRevisionsRequest,
3333
SearchRoutesRequest,
3434
UpdateDeploymentRequest,
@@ -90,11 +90,12 @@
9090
# Request DTOs - Path params
9191
"DeploymentPathParam",
9292
"DeploymentPolicyPathParam",
93+
"DeploymentProjectSearchPathParam",
9394
"RevisionPathParam",
9495
"RoutePathParam",
9596
# Request DTOs - Search/List
9697
"SearchDeploymentPoliciesRequest",
97-
"SearchDeploymentsRequest",
98+
"SearchLegacyDeploymentsRequest",
9899
"SearchRevisionsRequest",
99100
"SearchRoutesRequest",
100101
# Request DTOs - Create inputs

0 commit comments

Comments
 (0)