Skip to content

refactor(BA-5979): split deployment search into admin and scoped layers#11522

Open
jopemachine wants to merge 6 commits into
mainfrom
refactor/BA-5979-deployment-admin-repository
Open

refactor(BA-5979): split deployment search into admin and scoped layers#11522
jopemachine wants to merge 6 commits into
mainfrom
refactor/BA-5979-deployment-admin-repository

Conversation

@jopemachine
Copy link
Copy Markdown
Member

@jopemachine jopemachine commented May 8, 2026

Summary

Split the deployment search/projection paths so each axis (admin / user / project / project-summary / GraphQL DataLoader) has a dedicated action and repository method.

  • API path (new, v2) for create / update / get / activate_revision reads through EndpointRow.to_model_deployment_data() directly at the db_source/ boundary, bypassing the DeploymentInfo intermediate.
  • No-scope admin queries live in a new DeploymentAdminRepository + DeploymentAdminService + DeploymentAdminProcessors package, mirroring the vfolder / login_client_type admin-split convention.
  • User-scoped (my_search) and project-scoped (project_search) reads each get their own Search{User,Project}DeploymentsAction and a {User,Project}DeploymentSearchScope; the scope filter lives in the repository, not on the adapter as an injected base_condition.
  • Legacy v1 REST POST /deployments/search keeps its no-path-segment shape so existing CLI/SDK callers don't break — the project scope now travels inline on the body via the new SearchLegacyDeploymentsRequest (just SearchDeploymentsRequest + a required project_id), and the handler resolves it to a ProjectDeploymentSearchScope and routes through the same search_project_deployments processor as v2.
  • GraphQL DataLoader (batch_load_by_ids) routes through admin_search_deployments (the only remaining unscoped search after the legacy no-scope action was dropped). The parent resolver has already authorised access to whatever references these IDs, so the admin processor is acceptable here — the admin label is enforced at the resolver, not at the DataLoader.

Resolves BA-5979. Builds on top of #11494 (BA-5963) which is already on main.

Layer-by-layer changes

Models

File Before After
models/endpoint/row.py to_deployment_info only adds to_model_deployment_data() — projects directly to ModelDeploymentData (used by every new ModelDeploymentData-returning DB-source method)

Repository

Repository Method Before After
DeploymentRepository get_endpoint_info returns DeploymentInfo unchanged (write helpers / lifecycle still use it)
DeploymentRepository get_deployment_data new — direct ModelDeploymentData for the API path
DeploymentRepository search_endpoints returns DeploymentInfo unchanged (only internal callers remain)
DeploymentRepository search_user_deployments new — user-scoped, returns ModelDeploymentData
DeploymentRepository search_project_deployments new — project-scoped, returns ModelDeploymentData
DeploymentRepository search_project_deployment_summary new (renamed from search_deployments_in_project) — still backs project admin list pages with DeploymentSummaryData
DeploymentAdminRepository search_deployments new — admin (no-scope) projection straight to ModelDeploymentData

repositories/deployment/db_source/db_source.py

Helper / field Before After
EndpointRow.to_model_deployment_data() called by every new ModelDeploymentData-returning DB-source method instead of routing through DeploymentInfo
DeploymentDBSource._storage_manager held removed — never read; storage I/O lives on DeploymentStorageSource (still owned by DeploymentRepository)

Scopes

Scope Before After
ProjectDeploymentSearchScope exists unchanged
UserDeploymentSearchScope newEndpointRow.created_user == user_id

Action

Action Before After
SearchDeploymentsAction exists (no-scope, used by every search path) dropped — every search path now has a scope or lives under the admin processor
AdminSearchDeploymentsAction new, lives under the admin package (admin-only callers + DataLoader)
SearchUserDeploymentsAction new — user-scoped
SearchProjectDeploymentsAction new — project-scoped, returns ModelDeploymentData
SearchProjectDeploymentSummaryAction existed as SearchDeploymentsInProjectAction renamed — returns DeploymentSummaryData for project admin list pages

Service

Service Handler Before After
DeploymentService create_deployment get_endpoint_info + _convert_deployment_info_to_data get_deployment_data
DeploymentService update_deployment controller returns DeploymentInfo → convert controller updates, then get_deployment_data
DeploymentService get_deployment_by_id get_endpoint_info + convert get_deployment_data
DeploymentService activate_revision controller returns DeploymentInfo → convert controller activates, then get_deployment_data
DeploymentService search_user_deployments new — user-scoped
DeploymentService search_project_deployments new — project-scoped, returns ModelDeploymentData; serves both the v2 adapter and the legacy v1 REST handler
DeploymentService search_project_deployment_summary renamed from search_deployments_in_project; still returns the lighter DeploymentSummaryData
DeploymentAdminService admin_search_deployments new — calls DeploymentAdminRepository.search_deployments
DeploymentService (private) _convert_deployment_info_to_data existed removed with the legacy converter

Processor

Processor Field Before After
DeploymentProcessors search_deployments exists (no-scope) dropped
DeploymentProcessors search_user_deployments new
DeploymentProcessors search_project_deployments new
DeploymentProcessors search_project_deployment_summary renamed from search_deployments_in_project
DeploymentAdminProcessors (new) admin_search_deployments new package, registered in the top-level Processors

Adapter routing

Adapter method Before action After action
admin_search SearchDeploymentsAction (regular processor) AdminSearchDeploymentsAction (admin processor)
my_search SearchDeploymentsAction + created_user==user_id base-condition SearchUserDeploymentsAction + UserDeploymentSearchScope (regular processor)
project_search SearchDeploymentsAction + project==project_id base-condition SearchProjectDeploymentsAction + ProjectDeploymentSearchScope (regular processor)
batch_load_by_ids (DataLoader) SearchDeploymentsAction + by_ids condition AdminSearchDeploymentsAction + by_ids condition (admin processor; the parent resolver already authorised access)

Legacy v1 REST handler

File Change
api/rest/deployment/handler.py POST /deployments/search keeps its no-path-segment URL but now takes a SearchLegacyDeploymentsRequest body (project_id required), builds a ProjectDeploymentSearchScope, and routes through the shared search_project_deployments processor. RBAC therefore now enforces project-scoped MODEL_DEPLOYMENT:READ on this endpoint.
api/rest/tree.py unchanged from origin/main
client/cli/deployment.py v1 ./bai deployment list now requires --project-id.

Tests

File Change
tests/unit/manager/repositories/deployment/test_endpoint_projection.py new unit test for EndpointRow.to_model_deployment_data() covering reversed revisions order, dangling current_revision references, and the lifecycle status mapping
tests/component/deployment/conftest.py new regular_user_project_model_deployment_read_permission fixture — grants PROJECT-scoped MODEL_DEPLOYMENT:READ to regular_user_fixture so legacy-path regular-user search tests can pass the now-enforced RBAC check

Test plan

  • pants fmt fix lint check on every changed file
  • tests/unit/manager/repositories/deployment/test_endpoint_projection.py
  • tests/component/deployment/test_deployment_lifecycle.py::TestUserAccessDeployment::test_user_searches_empty_deployments (now seeds the project-scoped read role; previously passed only because the action was unscoped)
  • Live ./bai smoke after merge: admin search, my search, project search, GraphQL modelDeployment resolver (DataLoader), legacy POST /deployments/search
  • CI

🤖 Generated with Claude Code


📚 Documentation preview 📚: https://sorna--11522.org.readthedocs.build/en/11522/


📚 Documentation preview 📚: https://sorna-ko--11522.org.readthedocs.build/ko/11522/

@github-actions github-actions Bot added the size:XL 500~ LoC label May 8, 2026
@github-actions github-actions Bot added comp:manager Related to Manager component comp:common Related to Common component labels May 8, 2026
Comment thread src/ai/backend/manager/repositories/deployment/db_source/db_source.py Outdated
Comment thread src/ai/backend/manager/repositories/deployment/repository.py Outdated
Comment thread src/ai/backend/manager/repositories/deployment/repository.py Outdated
@github-actions github-actions Bot added comp:client Related to Client component area:docs Documentations comp:cli Related to CLI component labels May 15, 2026
@jopemachine jopemachine force-pushed the refactor/BA-5979-deployment-admin-repository branch from 6f84f80 to 4b8f701 Compare May 18, 2026 01:42
@jopemachine jopemachine marked this pull request as ready for review May 18, 2026 01:44
@jopemachine jopemachine requested a review from a team as a code owner May 18, 2026 01:44
Copilot AI review requested due to automatic review settings May 18, 2026 01:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Splits the deployment search/projection paths into a dedicated admin layer plus scoped (user / project) actions. The v2 API path now projects EndpointRow directly to ModelDeploymentData via a new to_model_deployment_data() method, bypassing the DeploymentInfo intermediate. A new DeploymentAdminRepository + DeploymentAdminService + DeploymentAdminProcessors package mirrors the vfolder/login-admin convention, and my_search / project_search get their own scopes and actions. The legacy v1 POST /deployments/search keeps its URL but now requires project_id in the body.

Changes:

  • New EndpointRow.to_model_deployment_data() and ModelDeploymentStatus.from_lifecycle(); service-side _convert_deployment_info_to_data removed.
  • New DeploymentAdminRepository / DeploymentAdminService / DeploymentAdminProcessors + AdminSearchDeploymentsAction; user/project scoped search actions and UserDeploymentSearchScope added; legacy v1 search routed through project scope.
  • DTO renames: AdminSearchDeploymentsInput/PayloadSearchDeploymentsInput/Payload; SearchDeploymentsRequestSearchLegacyDeploymentsRequest (requires project_id); CLI bai deployment list --project-id now required.

Reviewed changes

Copilot reviewed 45 out of 45 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/ai/backend/manager/models/endpoint/row.py Adds to_model_deployment_data() projection.
src/ai/backend/common/data/model_deployment/types.py Adds ModelDeploymentStatus.from_lifecycle().
src/ai/backend/manager/data/deployment/types.py Renames DeploymentInfoSearchResultModelDeploymentDataSearchResult.
src/ai/backend/manager/repositories/deployment/db_source/db_source.py Drops storage_manager from DBSource; adds get_deployment_data, admin_search_deployments, search_user_deployments, renames project search methods.
src/ai/backend/manager/repositories/deployment/admin_repository.py New admin repository owning unscoped search.
src/ai/backend/manager/repositories/deployment/repository.py / repositories.py / init.py Wire admin repository, add new scoped search methods.
src/ai/backend/manager/repositories/deployment/types/endpoint.py / init.py New UserDeploymentSearchScope.
src/ai/backend/manager/services/deployment/admin_service.py New admin service.
src/ai/backend/manager/services/deployment/processors.py Adds DeploymentAdminProcessors and scoped processors.
src/ai/backend/manager/services/deployment/service.py Removes legacy converter; switches create/update/get/activate to get_deployment_data; adds scoped search service methods.
src/ai/backend/manager/services/deployment/actions/admin_search_deployments.py / search_user_deployments.py / search_project_deployments.py / search_project_deployment_summary.py Action classes for the new split.
src/ai/backend/manager/services/factory.py / processors.py Register admin service + processors.
src/ai/backend/manager/api/adapters/deployment/adapter.py Routes admin_search / my_search / project_search / batch_load_by_ids to the new actions with scopes.
src/ai/backend/manager/api/rest/v2/deployment/handler.py Uses renamed SearchDeploymentsInput.
src/ai/backend/manager/api/rest/deployment/{handler,adapter}.py Legacy v1 search uses SearchLegacyDeploymentsRequest and routes through project-scoped action.
src/ai/backend/manager/api/gql/deployment/resolver/deployment.py Renamed input class.
src/ai/backend/common/dto/manager/{deployment,v2/deployment}/* DTO renames and shared SearchDeploymentsInput/Payload.
src/ai/backend/common/metrics/metric.py Adds DEPLOYMENT_ADMIN_REPOSITORY layer.
src/ai/backend/client/** Client/CLI renames; bai deployment list now requires --project-id.
tests/unit/manager/repositories/deployment/test_endpoint_projection.py New unit tests for the projection.
tests/unit/manager/repositories/deployment/test_search_project_deployment_summary.py Renames to match new repository method.
tests/unit/manager/services/deployment/test_deployment_service.py Removes tests for deleted converter.
tests/component/deployment/conftest.py Adds RBAC fixture granting PROJECT-scoped MODEL_DEPLOYMENT:READ.
tests/component/deployment/test_deployment*.py, tests/unit/client_v2/test_deployment.py Adopt renamed inputs and new RBAC fixture.
changes/11522.enhance.md News fragment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +415 to +432
async with self._begin_readonly_session_read_committed() as db_sess:
query = (
sa.select(EndpointRow)
.where(EndpointRow.id == endpoint_id)
.options(
selectinload(EndpointRow.revisions).selectinload(
DeploymentRevisionRow.image_row
),
selectinload(EndpointRow.deployment_policy),
)
)
result = await db_sess.execute(query)
row: EndpointRow | None = result.scalar_one_or_none()

if not row:
raise EndpointNotFound(f"Endpoint {endpoint_id} not found")

return row.to_model_deployment_data()
case EndpointLifecycle.DESTROYING:
return cls.STOPPING
case EndpointLifecycle.DESTROYED:
return cls.STOPPED
Comment thread src/ai/backend/manager/models/endpoint/row.py Outdated
Comment thread src/ai/backend/manager/repositories/deployment/admin_repository.py
Comment thread src/ai/backend/client/cli/deployment.py
Comment on lines 1356 to 1379
) -> list[DeploymentNode | None]:
"""Batch load deployments by ID for DataLoader use.

Returns DeploymentNode DTOs in the same order as the input deployment_ids list.
Routed through ``admin_search_deployments`` — the only remaining
unscoped search after the legacy path was removed. The ``by_ids``
filter is the bound on the result set; the parent resolver has
already authorised access to whatever entity references these IDs,
and the action is unscoped at the service layer (the admin label
is enforced at the resolver, not here).

Output is aligned with the input ``deployment_ids`` order; missing
IDs come back as ``None``.
"""
if not deployment_ids:
return []
querier = BatchQuerier(
pagination=OffsetPagination(limit=len(deployment_ids)),
conditions=[DeploymentConditions.by_ids(deployment_ids)],
)
action_result = await self._processors.deployment.search_deployments.wait_for_complete(
SearchDeploymentsAction(querier=querier)
action_result = (
await self._processors.deployment_admin.admin_search_deployments.wait_for_complete(
AdminSearchDeploymentsAction(querier=querier)
)
)
Comment on lines +91 to +122
def _build(
*,
current_revision: DeploymentRevisionID | None = None,
deploying_revision: DeploymentRevisionID | None = None,
current_revision_row: Any = None,
deploying_revision_row: Any = None,
lifecycle_stage: EndpointLifecycle = EndpointLifecycle.DEPLOYING,
) -> Any:
stub = MagicMock()
stub.id = DeploymentID(uuid.uuid4())
stub.name = "test-deployment"
stub.lifecycle_stage = lifecycle_stage
stub.tag = None
stub.project = uuid.uuid4()
stub.domain = "default"
stub.created_at = datetime(2024, 1, 1, tzinfo=UTC)
stub.open_to_public = False
stub.url = None
stub.current_revision = current_revision
stub.deploying_revision = deploying_revision
stub.current_revision_row = current_revision_row
stub.deploying_revision_row = deploying_revision_row
stub.replicas = 1
stub.desired_replicas = None
stub.deployment_policy = None
stub.created_user = uuid.uuid4()
stub.options = DeploymentOptions()
stub.scaling_state = ScalingState.STABLE
stub.sub_step = None
return stub

return _build
Comment thread src/ai/backend/common/data/model_deployment/types.py Outdated
@jopemachine jopemachine force-pushed the refactor/BA-5979-deployment-admin-repository branch from db1f00b to 4b8f701 Compare May 18, 2026 01:52
@jopemachine jopemachine marked this pull request as draft May 18, 2026 02:22
jopemachine and others added 5 commits May 18, 2026 12:17
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>
The DBSource's ``get_deployment_data`` was still selectinload-ing
``EndpointRow.revisions`` while the projection method
``to_model_deployment_data`` reads ``current_revision_row`` /
``deploying_revision_row`` directly. Those relationships default to
``lazy="select"``, so accessing them on a row fetched under an async
session would trip the SQLAlchemy ``greenlet_spawn`` error on every API
path that calls ``get_deployment_data`` (``create_deployment``,
``update_deployment``, ``get_deployment_by_id``, ``activate_revision``).

Align the eager-load with what ``admin_search_deployments`` /
``search_user_deployments`` / ``search_project_deployments`` already do,
and drop the now-unused ``revisions`` chain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… shape

Drop nested revision / policy / id-list payloads from
``ModelDeploymentData`` so the projection only ever carries the endpoint
row's own columns. The v2 GQL node already exposes only scope IDs and
defers the revision spec / policy / replica list / auto-scaling rules /
access tokens to dedicated DataLoader resolvers; the v1 REST surface
now mirrors that — clients fetch the spec through the nested endpoints
(``/deployments/{id}/revisions/{revision_id}``,
``/deployments/{id}/policy``, etc.). **Breaking change for v1 REST**:
``DeploymentDTO.current_revision`` and ``DeploymentDTO.deployment_policy``
are removed; ``current_revision_id`` and ``deploying_revision_id`` are
exposed instead.

With the projection no longer touching ``EndpointRow.current_revision_row``
/ ``deploying_revision_row`` / ``deployment_policy``, the four search /
get paths that consume ``to_model_deployment_data`` drop their
``selectinload`` chains — each was incurring a per-row dead eager load.
The v2 ``DeploymentNode.policy`` field is removed for the same reason
(GQL never read it; the resolver always went through the policy
DataLoader). The v1 handler's revision-variant resolver is no longer
needed on the deployment DTO path; ``_deployment_dto`` becomes
synchronous.

Tests:
- ``test_endpoint_projection`` drops the now-obsolete revision-row
  scenarios (the BA-5963 list-order regression is structurally
  impossible once the projection only reads columns) and keeps the
  column-pass-through plus the lifecycle status mapping pins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…factor

a70582c dropped the ``policy`` field from ``DeploymentNode`` (v2 GQL
response) but left this test module still building / asserting on it,
so mypy hit ``"DeploymentNode" has no attribute "policy"`` in 7 spots
and the typecheck CI job failed. Remove the ``policy`` default from the
test factory, drop the three now-meaningless policy test cases, and
prune the imports they exclusively used.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1 ``DeploymentDTO.sub_step`` had no v2 counterpart, so the GQL
``ModelDeployment`` node and the v2 REST ``DeploymentNode`` were missing
the rolling-update progress signal. Promote ``DeploymentLifecycleSubStep``
to ``common.data.model_deployment.types`` so the v2 DTO layer can reach it
(``common/`` cannot import from ``manager/``), repoint every existing
importer at the common location, then surface the enum on:

- ``DeploymentNode.sub_step`` (v2 REST DTO)
- ``ModelDeployment.subStep`` (GQL, backed by a new
  ``DeploymentLifecycleSubStep`` GQL enum)

The v2 adapter pipes ``ModelDeploymentData.sub_step`` through to the
new DTO field, and the v2 schema dump picks up the enum + field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jopemachine jopemachine force-pushed the refactor/BA-5979-deployment-admin-repository branch from 0c2a928 to 646ffdb Compare May 18, 2026 03:21
@jopemachine jopemachine marked this pull request as ready for review May 18, 2026 03:21
CI typecheck on the previous commit failed because three sokovan
deployment test files still pulled ``DeploymentLifecycleSubStep`` from
``ai.backend.manager.data.deployment.types`` — that re-export was the
casualty of moving the enum to ``common.data.model_deployment.types``.
Update the test imports to the common location to match every other
manager/sokovan caller migrated in the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jopemachine jopemachine requested a review from HyeockJinKim May 18, 2026 03:35
@jopemachine jopemachine requested a review from a team May 18, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentations comp:cli Related to CLI component comp:client Related to Client component comp:common Related to Common component comp:manager Related to Manager component size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants