refactor(BA-5979): split deployment search into admin and scoped layers#11522
Draft
jopemachine wants to merge 29 commits into
Draft
refactor(BA-5979): split deployment search into admin and scoped layers#11522jopemachine wants to merge 29 commits into
jopemachine wants to merge 29 commits into
Conversation
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.
…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).
jopemachine
commented
May 8, 2026
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.
jopemachine
commented
May 10, 2026
jopemachine
commented
May 10, 2026
jopemachine
commented
May 10, 2026
…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.
…xisting slot" This reverts commit 63e704a.
…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.
Co-authored-by: octodog <mu001@lablup.com>
…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.
… 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>
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Split the deployment search/projection paths so each axis (admin / user / project / project-summary / GraphQL DataLoader) has a dedicated action and repository method.
create/update/get/activate_revisionreads throughEndpointRow.to_model_deployment_data()directly at thedb_source/boundary, bypassing theDeploymentInfointermediate.DeploymentAdminRepository+DeploymentAdminService+DeploymentAdminProcessorspackage, mirroring thevfolder/login_client_typeadmin-split convention.my_search) and project-scoped (project_search) reads each get their ownSearch{User,Project}DeploymentsActionand a{User,Project}DeploymentSearchScope; the scope filter lives in the repository, not on the adapter as an injectedbase_condition.POST /deployments/searchkeeps its no-path-segment shape so existing CLI/SDK callers don't break — the project scope now travels inline on the body via the newSearchLegacyDeploymentsRequest(justSearchDeploymentsRequest+ a requiredproject_id), and the handler resolves it to aProjectDeploymentSearchScopeand routes through the samesearch_project_deploymentsprocessor as v2.batch_load_by_ids) routes throughadmin_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
models/endpoint/row.pyto_deployment_infoonlyto_model_deployment_data()— projects directly toModelDeploymentData(used by every new ModelDeploymentData-returning DB-source method)Repository
DeploymentRepositoryget_endpoint_infoDeploymentInfoDeploymentRepositoryget_deployment_dataModelDeploymentDatafor the API pathDeploymentRepositorysearch_endpointsDeploymentInfoDeploymentRepositorysearch_user_deploymentsModelDeploymentDataDeploymentRepositorysearch_project_deploymentsModelDeploymentDataDeploymentRepositorysearch_project_deployment_summarysearch_deployments_in_project) — still backs project admin list pages withDeploymentSummaryDataDeploymentAdminRepositorysearch_deploymentsModelDeploymentDatarepositories/deployment/db_source/db_source.pyEndpointRow.to_model_deployment_data()DeploymentInfoDeploymentDBSource._storage_managerDeploymentStorageSource(still owned byDeploymentRepository)Scopes
ProjectDeploymentSearchScopeUserDeploymentSearchScopeEndpointRow.created_user == user_idAction
SearchDeploymentsActionAdminSearchDeploymentsActionSearchUserDeploymentsActionSearchProjectDeploymentsActionModelDeploymentDataSearchProjectDeploymentSummaryActionSearchDeploymentsInProjectActionDeploymentSummaryDatafor project admin list pagesService
DeploymentServicecreate_deploymentget_endpoint_info+_convert_deployment_info_to_dataget_deployment_dataDeploymentServiceupdate_deploymentDeploymentInfo→ convertget_deployment_dataDeploymentServiceget_deployment_by_idget_endpoint_info+ convertget_deployment_dataDeploymentServiceactivate_revisionDeploymentInfo→ convertget_deployment_dataDeploymentServicesearch_user_deploymentsDeploymentServicesearch_project_deploymentsModelDeploymentData; serves both the v2 adapter and the legacy v1 REST handlerDeploymentServicesearch_project_deployment_summarysearch_deployments_in_project; still returns the lighterDeploymentSummaryDataDeploymentAdminServiceadmin_search_deploymentsDeploymentAdminRepository.search_deploymentsDeploymentService(private)_convert_deployment_info_to_dataProcessor
DeploymentProcessorssearch_deploymentsDeploymentProcessorssearch_user_deploymentsDeploymentProcessorssearch_project_deploymentsDeploymentProcessorssearch_project_deployment_summarysearch_deployments_in_projectDeploymentAdminProcessors(new)admin_search_deploymentsProcessorsAdapter routing
admin_searchSearchDeploymentsAction(regular processor)AdminSearchDeploymentsAction(admin processor)my_searchSearchDeploymentsAction+created_user==user_idbase-conditionSearchUserDeploymentsAction+UserDeploymentSearchScope(regular processor)project_searchSearchDeploymentsAction+project==project_idbase-conditionSearchProjectDeploymentsAction+ProjectDeploymentSearchScope(regular processor)batch_load_by_ids(DataLoader)SearchDeploymentsAction+by_idsconditionAdminSearchDeploymentsAction+by_idscondition (admin processor; the parent resolver already authorised access)Legacy v1 REST handler
api/rest/deployment/handler.pyPOST /deployments/searchkeeps its no-path-segment URL but now takes aSearchLegacyDeploymentsRequestbody (project_id required), builds aProjectDeploymentSearchScope, and routes through the sharedsearch_project_deploymentsprocessor. RBAC therefore now enforces project-scopedMODEL_DEPLOYMENT:READon this endpoint.api/rest/tree.pyorigin/mainclient/cli/deployment.py./bai deployment listnow requires--project-id.Tests
tests/unit/manager/repositories/deployment/test_endpoint_projection.pyEndpointRow.to_model_deployment_data()covering reversedrevisionsorder, danglingcurrent_revisionreferences, and the lifecycle status mappingtests/component/deployment/conftest.pyregular_user_project_model_deployment_read_permissionfixture — grants PROJECT-scopedMODEL_DEPLOYMENT:READtoregular_user_fixtureso legacy-path regular-user search tests can pass the now-enforced RBAC checkTest plan
pants fmt fix lint checkon every changed filetests/unit/manager/repositories/deployment/test_endpoint_projection.pytests/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)./baismoke after merge: admin search, my search, project search, GraphQLmodelDeploymentresolver (DataLoader), legacyPOST /deployments/search🤖 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/