Skip to content

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

Draft
jopemachine wants to merge 8 commits intomainfrom
refactor/BA-5979-deployment-admin-repository
Draft

refactor(BA-5979): split deployment search into admin and scoped layers#11522
jopemachine wants to merge 8 commits intomainfrom
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 / legacy / DataLoader) has a dedicated action and repository method.

  • API path (new, v2) for create / update / get / activate_revision reads through EndpointRow -> ModelDeploymentData directly via the private helper _endpoint_row_to_model_deployment_data at the db_source/ boundary.
  • 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}ModelDeploymentsAction + *DeploymentSearchScope; the scope filter lives in the repository, not on the adapter as an injected base_condition.
  • Naming: every method/action/processor field that returns ModelDeploymentData reads *_model_deployments / *ModelDeployments*, matching the entity term carried by the data type (parallel to search_vfolders <- VFolderData). Scope class names keep *DeploymentSearchScope because the scope describes the entity being filtered, not the return type.
  • Legacy v1 REST keeps its own SearchLegacyDeploymentsAction (renamed from SearchDeploymentsAction) and continues to project via DeploymentInfo -> _convert_deployment_info_to_data -> ModelDeploymentData.
  • GraphQL DataLoader (batch_load_by_ids) shares SearchLegacyDeploymentsAction for now; the admin processor was a wrong fit because DataLoader fires under the parent resolver's user (admin or not). Renaming the shared action so it does not read as legacy for the v2 DataLoader path is left to a follow-up.

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 unchanged — no API-shaped projection added to the row

Repository

Repository Method Before After
DeploymentRepository get_endpoint_info returns DeploymentInfo unchanged (legacy path still uses it)
DeploymentRepository get_model_deployment_data new — direct ModelDeploymentData for the API path
DeploymentRepository search_endpoints returns DeploymentInfo unchanged (legacy path)
DeploymentRepository search_user_model_deployments new — user-scoped, returns ModelDeploymentData
DeploymentRepository search_project_model_deployments new — project-scoped, returns ModelDeploymentData
DeploymentRepository search_deployments_in_project returns DeploymentSummaryData unchanged — still backs project admin list pages
DeploymentAdminRepository search_model_deployments new — admin (no-scope) projection straight to ModelDeploymentData

repositories/deployment/db_source/db_source.py (helper functions)

Helper Before After
_lifecycle_to_status new module-private function — used by the API-path projection
_endpoint_row_to_model_deployment_data new module-private function — projects EndpointRow straight to ModelDeploymentData; called by every new ModelDeploymentData-returning DB-source method
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) renamed to SearchLegacyDeploymentsAction (file actions/search_legacy_deployments.py); now serves the v1 REST handler and the v2 GraphQL DataLoader
AdminSearchModelDeploymentsAction new, lives under the admin package (admin-only callers)
SearchUserModelDeploymentsAction new — user-scoped
SearchProjectModelDeploymentsAction new — project-scoped, returns ModelDeploymentData
SearchDeploymentsInProjectAction exists, returns DeploymentSummaryData unchanged

Service

Service Handler Before After
DeploymentService create_deployment get_endpoint_info + _convert_deployment_info_to_data get_model_deployment_data
DeploymentService update_deployment controller returns DeploymentInfo -> convert controller updates, then get_model_deployment_data
DeploymentService get_deployment_by_id get_endpoint_info + convert get_model_deployment_data
DeploymentService activate_revision controller returns DeploymentInfo -> convert controller activates, then get_model_deployment_data
DeploymentService search_deployments -> search_legacy_deployments search_endpoints + _convert_deployment_info_to_data unchanged behaviour (only the method name) — legacy path preserved
DeploymentService search_user_model_deployments new — user-scoped
DeploymentService search_project_model_deployments new — project-scoped, returns ModelDeploymentData
DeploymentAdminService admin_search_model_deployments new — calls DeploymentAdminRepository.search_model_deployments

Processor

Processor Field Before After
DeploymentProcessors search_deployments exists renamed to search_legacy_deployments
DeploymentProcessors search_user_model_deployments new
DeploymentProcessors search_project_model_deployments new
DeploymentAdminProcessors (new) admin_search_model_deployments new package, registered in the top-level Processors

Adapter routing

Adapter method Before action After action
admin_search SearchDeploymentsAction (regular processor) AdminSearchModelDeploymentsAction (admin processor)
my_search SearchDeploymentsAction + created_user==user_id base-condition SearchUserModelDeploymentsAction + UserDeploymentSearchScope (regular processor)
project_search SearchDeploymentsAction + project==project_id base-condition SearchProjectModelDeploymentsAction + ProjectDeploymentSearchScope (regular processor)
batch_load_by_ids (DataLoader) SearchDeploymentsAction + by_ids condition SearchLegacyDeploymentsAction + by_ids condition (regular processor — admin processor is admin-only and DataLoader fires for every user)

Legacy v1 REST handler

File Change
api/rest/deployment/handler.py only the import line + wait_for_complete reference are renamed (SearchDeploymentsAction -> SearchLegacyDeploymentsAction); logic untouched
api/rest/tree.py unchanged from origin/main

Service-layer helpers (legacy converter)

Helper Before After
_map_lifecycle_to_status (service.py) exists unchanged — still used by the legacy converter
_convert_deployment_info_to_data (service.py) exists unchanged — still used by search_legacy_deployments

Known limitation / follow-up

SearchLegacyDeploymentsAction is shared between the v1 REST handler and the v2 GraphQL DataLoader. The action's name reads as legacy, which is accurate for the v1 REST consumer but not for the DataLoader path — splitting it (e.g. into a dedicated BatchLoadDeploymentsByIdsAction) is intentionally deferred so this PR does not touch the v1 REST contract surface.

Test plan

  • pants fmt fix lint check on every changed file
  • tests/unit/manager/repositories/deployment/test_endpoint_projection.py — new unit test for _endpoint_row_to_model_deployment_data covering reversed revisions order, dangling current_revision references, and the lifecycle status mapping
  • Live ./bai smoke after merge: admin search, my search, project search, GraphQL modelDeployment resolver (DataLoader), legacy search_deployments REST endpoint
  • CI

🤖 Generated with Claude Code

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).
log = BraceStyleAdapter(logging.getLogger(__name__))


def _lifecycle_to_status(lifecycle: EndpointLifecycle) -> ModelDeploymentStatus:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this should be included adapter

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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