Skip to content

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

Draft
jopemachine wants to merge 29 commits into
mainfrom
refactor/BA-5979-deployment-admin-repository
Draft

refactor(BA-5979): split deployment search into admin and scoped layers#11522
jopemachine wants to merge 29 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/

Move ``search_model_deployments`` (no-scope, returns ``ModelDeploymentData``
directly from ``EndpointRow``) into a dedicated ``DeploymentAdminRepository``
and route it through a new ``DeploymentAdminService`` /
``DeploymentAdminProcessors`` package. The corresponding action is renamed
to ``AdminSearchDeploymentsAction`` so its admin (no-scope) intent is
explicit at every layer, mirroring the ``vfolder`` / ``login_client_type``
admin-split convention.

The five service paths that previously called ``_convert_deployment_info_to_data``
on a freshly fetched ``DeploymentInfo`` (``create_deployment``,
``update_deployment``, ``get_deployment_by_id``, ``activate_revision``, plus
the search path) now read straight from ``EndpointRow.to_model_deployment_data``
via the repository, so revision-id columns flow through the API path
unchanged from the DB and the ``model_revisions`` ordering ambiguity that
BA-5963 patched out becomes unreachable structurally.

The legacy REST handler and the v2 GraphQL adapter both consume the new
``deployment_admin`` processor; the orphaned ``SearchDeploymentsAction``
file is removed. ``DeploymentAdminRepository`` shares the underlying
``DeploymentDBSource``, so SQL-level query logic stays in one place — only
the layering above it is split.

Resolves BA-5979.
@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
…mentsAction

Walks back the over-eager rewiring from the previous BA-5979 commit:

- Restore ``_convert_deployment_info_to_data`` (and its
  ``_map_lifecycle_to_status`` helper) on ``DeploymentService`` for the
  legacy projection path.
- Rename the action to ``SearchLegacyDeploymentsAction`` (file
  ``services/deployment/actions/search_legacy_deployments.py``) so the
  legacy intent is explicit; keep the original
  ``search_endpoints`` -> ``DeploymentInfo`` -> converter -> ``ModelDeploymentData``
  pipeline intact in the service handler.
- Adapter ``my_search`` / ``project_search`` and the GraphQL
  ``batch_load_by_ids`` go back to the regular processor's
  ``search_legacy_deployments``; only ``admin_search`` stays on the new
  ``DeploymentAdminProcessors.admin_search_deployments``.
- The legacy v1 REST handler and ``tree.py`` are reverted to ``origin/main``
  except for the import / call-name update forced by the action rename.
- Drop the now-unused ``search_model_deployments`` helper from
  ``DeploymentRepository`` so the no-scope query is owned by
  ``DeploymentAdminRepository`` alone.
…he model layer

The previous commit attached ``to_model_deployment_data`` and the
``_lifecycle_to_status`` helper to ``EndpointRow``, but the conversion is
purely an adapter between the ORM row and the API-shaped data type — that
is not a model-layer concern. Move both into
``repositories/deployment/db_source/db_source.py`` as private free
functions (``_endpoint_row_to_model_deployment_data`` and
``_lifecycle_to_status``); ``get_model_deployment_data`` and
``search_model_deployments`` now call the helper directly.

The companion unit test moves to
``tests/unit/manager/repositories/deployment/test_endpoint_projection.py``
to match the new home, and the component test reverts to importing
``_map_lifecycle_to_status`` from ``services/deployment/service.py``
(its origin/main location).
Comment thread src/ai/backend/manager/repositories/deployment/db_source/db_source.py Outdated
Following the vfolder convention so v2 paths no longer borrow the
``search_legacy_deployments`` action.

- Add ``UserDeploymentSearchScope`` (alongside the existing
  ``ProjectDeploymentSearchScope``) so the user-scope filter
  (``EndpointRow.created_user == user_id``) lives at the repository
  boundary instead of being injected at the adapter via
  ``base_conditions``.
- Add ``SearchUserDeploymentsAction`` and ``SearchProjectDeploymentsAction``
  (returning ``ModelDeploymentData``) plus matching service handlers,
  processor fields, and repository / DB-source methods. The internal
  scope-name is ``User``; the v2 adapter exposes it as ``my_search``.
- Adapter:
  - ``my_search`` -> ``search_user_deployments``
  - ``project_search`` -> ``search_project_deployments``
  - ``batch_load_by_ids`` (DataLoader) -> ``admin_search_deployments`` —
    the ``by_ids`` filter is itself the bound on the result set, so the
    no-scope admin processor is the right home; this also keeps
    ``search_legacy_deployments`` reserved for the v1 REST handler.

The existing ``SearchDeploymentsInProjectAction`` (returning
``DeploymentSummaryData``) is unchanged — its lighter shape is still
what project admin list pages consume.
…egacy_deployments

Going through ``DeploymentAdminProcessors.admin_search_deployments`` made
the DataLoader path admin-only, which breaks any non-admin GraphQL query
that resolves a deployment reference (e.g. ``ModelDeploymentNode`` from a
sibling entity). The admin processor exists to mark callers that *must*
be admin-authorised; a DataLoader inherits the parent resolver's
authorisation and runs for every user, so it cannot live there.

Route ``batch_load_by_ids`` through the regular
``DeploymentProcessors.search_legacy_deployments`` until a dedicated
non-admin batch action lands (the action's ``legacy`` name is awkward
for a v2 DataLoader path; renaming is left to a follow-up so the v1
REST contract stays untouched in this PR).
The dependency was held on the DB source but never read by any of its
methods — storage I/O lives on ``DeploymentStorageSource`` (still owned by
``DeploymentRepository``). Drop the field, the ctor parameter, and the
forwarding through both repositories.
…ployments` consistently

The admin repo had `search_model_deployments`, but the new user/project
counterparts and matching actions/handlers/processors used the shorter
`deployments`. Align them: methods, actions, handlers, processor fields,
and action file names that return `ModelDeploymentData` now all read
`*_model_deployments` / `*ModelDeployments*`. The legacy
`search_legacy_deployments` (returns `ModelDeploymentData` via the
`DeploymentInfo` converter) keeps its name to preserve the v1 REST
contract surface, and `*DeploymentSearchScope` keeps its name because
the scope describes the entity being filtered, not the return type.

Renames:
- AdminSearchDeploymentsAction -> AdminSearchModelDeploymentsAction
- SearchUserDeploymentsAction -> SearchUserModelDeploymentsAction
- SearchProjectDeploymentsAction -> SearchProjectModelDeploymentsAction
- service.{admin_search,search_user,search_project}_deployments -> *_model_deployments
- processor fields and action file paths follow.
Comment thread src/ai/backend/manager/repositories/deployment/repository.py Outdated
Comment thread src/ai/backend/manager/repositories/deployment/repository.py Outdated
jopemachine and others added 8 commits May 11, 2026 13:36
…oyment-admin-repository

# Conflicts:
#	src/ai/backend/manager/repositories/deployment/db_source/db_source.py
#	src/ai/backend/manager/services/deployment/service.py
#	tests/unit/manager/services/deployment/test_deployment_service.py
- Dedupe `EndpointLifecycle` -> `ModelDeploymentStatus` mapping into
  `ModelDeploymentStatus.from_lifecycle`; drop the per-module copies in
  `db_source.py` and `service.py`.
- Tighten `endpoint_id` typing on `get_deployment_data` to `DeploymentID`
  on both repository and db_source signatures.
- Drop the redundant `model_` prefix on PR-added method/action/processor
  names (the data type itself stays `ModelDeploymentData`):
  * `get_model_deployment_data` -> `get_deployment_data`
  * `search_user_model_deployments` -> `search_user_deployments`
  * `search_project_model_deployments` -> `search_project_deployments`
  * admin: `search_model_deployments` -> `search_deployments`
  * `_endpoint_row_to_model_deployment_data` -> `_endpoint_row_to_deployment_data`
  * `Admin/User/Project SearchModelDeploymentsAction` -> `*SearchDeploymentsAction`
  * action files renamed to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`services/CLAUDE.md` mandates `@dataclass(frozen=True)` for every Action
and ActionResult. The four new search actions added in this PR were
missing the `frozen=True` flag — fix it on:
- AdminSearchDeploymentsAction(Result)
- SearchLegacyDeploymentsAction(Result)
- SearchUserDeploymentsAction(Result)
- SearchProjectDeploymentsAction(Result)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original docstring claimed admin and regular repositories "share"
the underlying ``DeploymentDBSource``, but each constructor builds its
own instance. Rewrite the comment to match reality: the DBSource is a
stateless query namespace bound to the shared engine, so every
repository in the package owns its own DBSource and reuse comes through
the single ``db_source/`` module rather than instance sharing.

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

# Conflicts:
#	src/ai/backend/manager/repositories/deployment/db_source/db_source.py
#	src/ai/backend/manager/repositories/deployment/repository.py
#	src/ai/backend/manager/repositories/deployment/types/__init__.py
…slot

Reuse the pre-existing search_deployments_in_project infrastructure
instead of adding a duplicate search_project_deployments. Promote
search_deployments_in_project to return ModelDeploymentData (matching
the user/admin search variants) and drop the now-unused
DeploymentSummaryData/to_summary_data path that had no callers.
…h to existing slot

Reuse the pre-existing project-scoped SearchDeploymentsInProjectAction
(returns DeploymentSummaryData) for the v2 project_search adapter
instead of adding a duplicate scoped action that returned full
ModelDeploymentData. Add a lighter response shape — DeploymentSummaryNode
+ ProjectSearchDeploymentsPayload (common DTO), ModelDeploymentSummary
+ Connection/Edge (Strawberry GQL), and an EndpointLifecycle GQL enum —
so the project list view surfaces only the scalar columns the
DeploymentSummaryData projection already carries. Pre-existing
DeploymentSummaryData / DeploymentSummarySearchResult / to_summary_data
remain untouched.
@github-actions github-actions Bot added the comp:client Related to Client component label May 15, 2026
Co-authored-by: octodog <mu001@lablup.com>
@github-actions github-actions Bot added the area:docs Documentations label May 15, 2026
…ct_search to existing slot"

This reverts commit b566693.
…tSummary

Make the two project-scoped search variants distinguishable by their
return shape:

- search_project_deployment_summary (was search_deployments_in_project) —
  lightweight DeploymentSummaryData, project list views.
- search_project_deployments — full ModelDeploymentData, project_search
  adapter / project_deployments GQL.

Renames action class, action result class, db_source method, repository
method, service method, processor field/spec, and the corresponding
test class/file.
jopemachine and others added 5 commits May 15, 2026 15:58
… search

Convert v1 REST POST /search to POST /projects/{project_id}/search backed
by SearchProjectDeploymentsAction, route the deployment GQL DataLoader
through admin_search_deployments, and remove SearchLegacyDeploymentsAction
together with its service/repository/db_source/data-type fallout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to fda845c — the same frozen=True enforcement that was applied
to SearchProjectDeployments and SearchUserDeployments was missed on
SearchProjectDeploymentSummaryAction / ...Result.

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

Promote the repository-side ``_endpoint_row_to_deployment_data`` helper to
``EndpointRow.to_model_deployment_data()`` so the row → API-data projection
sits alongside the existing ``to_deployment_info()`` (and mirrors
``EndpointAutoScalingRuleRow.to_model_deployment_data()``). Update the four
db_source call sites and drop the now-unused helper and imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Revert the v1 path from /projects/{project_id}/search back to /search so
existing legacy SDK callers (CLI, ./bai, third-party scripts) keep
working. The project scope now travels inside the body via a new
SearchLegacyDeploymentsRequest (subclass of SearchDeploymentsRequest +
required ``project_id``), which the v1 handler resolves to
ProjectDeploymentSearchScope and routes through the v2-shared
search_project_deployments processor.

CI test failures from the broken contract:
- tests/component/deployment/test_deployment.py (3 admin search cases)
- tests/component/deployment/test_deployment_lifecycle.py
  (test_user_searches_empty_deployments — 405 Method Not Allowed)
- tests/unit/client_v2/test_deployment.py (search URL assertion)
- tests/unit/manager/repositories/deployment/test_endpoint_projection.py
  (ImportError on _endpoint_row_to_deployment_data after the row→data
  projection moved onto EndpointRow.to_model_deployment_data)

Also pivot the v1 ./bai deployment list CLI to require --project-id and
update Client func/SDK signatures to take the new request type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the LoginSession v2 pattern: each search variant now carries its
own DTO class instead of all three reusing AdminSearchDeploymentsInput /
AdminSearchDeploymentsPayload.

- AdminSearch{Input,Payload} — unchanged, still wired to admin_search
- ProjectSearch{Input,Payload} — new, wired to project_search (path-scoped)
- MySearch{Input,Payload} — new, wired to my_search (current user)

All three classes share the same field shape; the split keeps the GQL /
OpenAPI schema accurate per scope, makes future divergence (per-scope
filter or pagination tweaks) a local change, and lines up the deployment
domain with login_session.

Touches all four layers: DTO ↔ adapter ↔ REST v2 handler ↔ GQL resolver
↔ SDK v2 client ↔ CLI v2 (admin/my/project) ↔ component tests for the
my_search and project_search call sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the comp:cli Related to CLI component label May 15, 2026
jopemachine and others added 4 commits May 15, 2026 17:20
… pair

Walk back the per-scope split from the previous commit. The admin /
project / my variants all share the same input shape and the same
result shape, and the prevailing v2 convention (vfolder, model_card,
user, session) is one neutral DTO pair per entity rather than three
scope-flavored ones. login_session is the lone outlier and not a
pattern we want to spread further.

- Drop ``MySearchDeploymentsInput`` / ``ProjectSearchDeploymentsInput``.
- Rename ``AdminSearchDeploymentsInput`` → ``SearchDeploymentsInput`` —
  the admin prefix was misleading once the type was reused across all
  three scopes.
- Rename ``AdminSearchDeploymentsPayload`` → ``SearchDeploymentsPayload``
  for the same reason.
- Re-point every consumer (adapter, REST v2 handler, GQL resolver,
  v2 SDK client, ./bai admin/my/project CLI, component tests) at the
  neutral pair; scope still travels via the URL path / handler choice.

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

Make SearchLegacyDeploymentsRequest a standalone BaseRequestModel rather
than a SearchDeploymentsRequest subclass — every legacy-only field
(project_id, filter, order, limit, offset) is now visible on the class
itself instead of being inherited, so the v1 contract reads
self-contained at a glance.

The base SearchDeploymentsRequest has no remaining callers after this
PR's migration (v1 SDK/CLI/func, REST adapter, and tests all use the
Legacy variant now), so drop it along with its __all__ entries.

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

The legacy ``POST /deployments/search`` path is now backed by the
project-scoped ``SearchProjectDeploymentsAction``, so its scope
RBAC validator requires a real PROJECT-scoped MODEL_DEPLOYMENT:READ
role on the caller. The previous ``SearchDeploymentsAction`` was
unscoped, which is why ``test_user_searches_empty_deployments``
used to pass without seeding any role.

Add a ``regular_user_project_model_deployment_read_permission``
fixture mirroring vfolder_v2's ``regular_user_vfolder_create_permission``,
and wire it into the failing test.

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

Align the projection unit test with the surrounding repository test
conventions:

- Promote ``_stub_endpoint`` / ``_stub_revision`` to factory fixtures
  (``endpoint_factory`` / ``revision_factory``) and pull lifecycle
  through the same factory parameter — tests no longer mutate the
  stub after build.
- Parametrize ``test_current_revision_resolved_by_id_match_not_list_order``
  over both revision orderings (deploying-first AND current-first) so
  the regression guard covers either DB row order.
- Parametrize ``test_lifecycle_status_mapping`` over every
  ``EndpointLifecycle`` -> ``ModelDeploymentStatus`` pair instead of
  spot-checking only the READY case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant