Skip to content

Commit c6946f2

Browse files
fregataaclaude
andauthored
feat(BA-6083): add ResolveDomainIDByName and ResolveResourceGroupIDByName actions (#11660)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 761cc8f commit c6946f2

13 files changed

Lines changed: 175 additions & 2 deletions

File tree

changes/11660.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `ResolveDomainIDByName` and `ResolveResourceGroupIDByName` actions to resolve entity names to their canonical UUID identifiers.

src/ai/backend/manager/repositories/domain/db_source/db_source.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sqlalchemy as sa
66

7+
from ai.backend.common.identifier.domain import DomainID, DomainName
78
from ai.backend.logging.utils import BraceStyleAdapter
89
from ai.backend.manager.data.domain.types import DomainData
910
from ai.backend.manager.errors.resource import DomainNotFound
@@ -41,6 +42,14 @@ async def get_domain(self, domain_name: str) -> DomainData:
4142
raise DomainNotFound(f"Domain '{domain_name}' not found")
4243
return row.to_data()
4344

45+
async def get_domain_id_by_name(self, name: DomainName) -> DomainID:
46+
async with self._db.begin_readonly_session_read_committed() as db_sess:
47+
query = sa.select(DomainRow.id).where(DomainRow.name == name)
48+
domain_id = await db_sess.scalar(query)
49+
if domain_id is None:
50+
raise DomainNotFound(f"Domain '{name}' not found")
51+
return domain_id
52+
4453
async def search_domains(self, querier: BatchQuerier) -> DomainSearchResult:
4554
"""Search all domains with pagination and filters.
4655

src/ai/backend/manager/repositories/domain/repository.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sqlalchemy.ext.asyncio import AsyncSession as SASession
88

99
from ai.backend.common.exception import BackendAIError, DomainNotFound, InvalidAPIParameters
10+
from ai.backend.common.identifier.domain import DomainID, DomainName
1011
from ai.backend.common.metrics.metric import DomainType, LayerType
1112
from ai.backend.common.resilience.policies.metrics import MetricArgs, MetricPolicy
1213
from ai.backend.common.resilience.policies.retry import BackoffStrategy, RetryArgs, RetryPolicy
@@ -380,6 +381,10 @@ async def _ensure_sgroup_permission(
380381

381382
# ==================== V2 Repository Methods ====================
382383

384+
@domain_repository_resilience.apply()
385+
async def get_domain_id_by_name(self, name: DomainName) -> DomainID:
386+
return await self._db_source.get_domain_id_by_name(name)
387+
383388
@domain_repository_resilience.apply()
384389
async def get_domain(self, domain_name: str) -> DomainData:
385390
"""Get a single domain by name.

src/ai/backend/manager/repositories/scaling_group/db_source/db_source.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.dialects.postgresql import insert as pg_insert
1010

1111
from ai.backend.common.data.permission.types import RBACElementType, RelationType
12-
from ai.backend.common.identifier.resource_group import ResourceGroupName
12+
from ai.backend.common.identifier.resource_group import ResourceGroupID, ResourceGroupName
1313
from ai.backend.common.types import SlotQuantity
1414
from ai.backend.manager.data.agent.types import AgentStatus
1515
from ai.backend.manager.data.deployment.types import DeploymentOptions
@@ -119,6 +119,14 @@ async def search_scaling_groups(
119119
has_previous_page=result.has_previous_page,
120120
)
121121

122+
async def get_resource_group_id_by_name(self, name: ResourceGroupName) -> ResourceGroupID:
123+
async with self._db.begin_readonly_session_read_committed() as db_sess:
124+
query = sa.select(ScalingGroupRow.id).where(ScalingGroupRow.name == name)
125+
resource_group_id = await db_sess.scalar(query)
126+
if resource_group_id is None:
127+
raise ScalingGroupNotFound(name)
128+
return resource_group_id
129+
122130
async def get_scaling_group_by_name(
123131
self,
124132
name: str,

src/ai/backend/manager/repositories/scaling_group/repository.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import TYPE_CHECKING
44
from uuid import UUID
55

6-
from ai.backend.common.identifier.resource_group import ResourceGroupName
6+
from ai.backend.common.identifier.resource_group import ResourceGroupID, ResourceGroupName
77
from ai.backend.common.metrics.metric import DomainType, LayerType
88
from ai.backend.common.resilience import (
99
MetricArgs,
@@ -89,6 +89,10 @@ async def search_scaling_groups(
8989
"""Searches scaling groups with total count."""
9090
return await self._db_source.search_scaling_groups(querier=querier)
9191

92+
@scaling_group_repository_resilience.apply()
93+
async def get_resource_group_id_by_name(self, name: ResourceGroupName) -> ResourceGroupID:
94+
return await self._db_source.get_resource_group_id_by_name(name)
95+
9296
@scaling_group_repository_resilience.apply()
9397
async def get_scaling_group_by_name(
9498
self,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import override
5+
6+
from ai.backend.common.identifier.domain import DomainID, DomainName
7+
from ai.backend.manager.actions.action import BaseActionResult
8+
from ai.backend.manager.actions.types import ActionOperationType
9+
from ai.backend.manager.services.domain.actions.base import DomainAction
10+
11+
12+
@dataclass
13+
class ResolveDomainIDByNameAction(DomainAction):
14+
name: DomainName
15+
16+
@override
17+
def entity_id(self) -> str | None:
18+
return str(self.name)
19+
20+
@override
21+
@classmethod
22+
def operation_type(cls) -> ActionOperationType:
23+
return ActionOperationType.GET
24+
25+
26+
@dataclass
27+
class ResolveDomainIDByNameActionResult(BaseActionResult):
28+
domain_id: DomainID
29+
30+
@override
31+
def entity_id(self) -> str | None:
32+
return str(self.domain_id)

src/ai/backend/manager/services/domain/processors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
PurgeDomainAction,
3333
PurgeDomainActionResult,
3434
)
35+
from ai.backend.manager.services.domain.actions.resolve_domain_id_by_name import (
36+
ResolveDomainIDByNameAction,
37+
ResolveDomainIDByNameActionResult,
38+
)
3539
from ai.backend.manager.services.domain.actions.search_domains import (
3640
SearchDomainsAction,
3741
SearchDomainsActionResult,
@@ -54,6 +58,9 @@ class DomainProcessors(AbstractProcessorPackage):
5458
get_domain: ActionProcessor[GetDomainAction, GetDomainActionResult]
5559
search_domains: ActionProcessor[SearchDomainsAction, SearchDomainsActionResult]
5660
search_rg_domains: ActionProcessor[SearchRGDomainsAction, SearchRGDomainsActionResult]
61+
resolve_domain_id_by_name: ActionProcessor[
62+
ResolveDomainIDByNameAction, ResolveDomainIDByNameActionResult
63+
]
5764

5865
def __init__(
5966
self,
@@ -70,6 +77,9 @@ def __init__(
7077
self.get_domain = ActionProcessor(service.get_domain, action_monitors)
7178
self.search_domains = ActionProcessor(service.search_domains, action_monitors)
7279
self.search_rg_domains = ActionProcessor(service.search_rg_domains, action_monitors)
80+
self.resolve_domain_id_by_name = ActionProcessor(
81+
service.resolve_domain_id_by_name, action_monitors
82+
)
7383

7484
@override
7585
def supported_actions(self) -> list[ActionSpec]:
@@ -83,4 +93,5 @@ def supported_actions(self) -> list[ActionSpec]:
8393
GetDomainAction.spec(),
8494
SearchDomainsAction.spec(),
8595
SearchRGDomainsAction.spec(),
96+
ResolveDomainIDByNameAction.spec(),
8697
]

src/ai/backend/manager/services/domain/service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
PurgeDomainAction,
3636
PurgeDomainActionResult,
3737
)
38+
from ai.backend.manager.services.domain.actions.resolve_domain_id_by_name import (
39+
ResolveDomainIDByNameAction,
40+
ResolveDomainIDByNameActionResult,
41+
)
3842
from ai.backend.manager.services.domain.actions.search_domains import (
3943
SearchDomainsAction,
4044
SearchDomainsActionResult,
@@ -126,6 +130,12 @@ async def modify_domain_node(
126130
domain_data=domain_data,
127131
)
128132

133+
async def resolve_domain_id_by_name(
134+
self, action: ResolveDomainIDByNameAction
135+
) -> ResolveDomainIDByNameActionResult:
136+
domain_id = await self._repository.get_domain_id_by_name(action.name)
137+
return ResolveDomainIDByNameActionResult(domain_id=domain_id)
138+
129139
async def get_domain(self, action: GetDomainAction) -> GetDomainActionResult:
130140
"""Get a single domain by name.
131141
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import override
5+
6+
from ai.backend.common.identifier.resource_group import ResourceGroupID, ResourceGroupName
7+
from ai.backend.manager.actions.action import BaseActionResult
8+
from ai.backend.manager.actions.types import ActionOperationType
9+
from ai.backend.manager.services.scaling_group.actions.base import ScalingGroupAction
10+
11+
12+
@dataclass(frozen=True)
13+
class ResolveResourceGroupIDByNameAction(ScalingGroupAction):
14+
name: ResourceGroupName
15+
16+
@override
17+
def entity_id(self) -> str | None:
18+
return str(self.name)
19+
20+
@override
21+
@classmethod
22+
def operation_type(cls) -> ActionOperationType:
23+
return ActionOperationType.GET
24+
25+
26+
@dataclass(frozen=True)
27+
class ResolveResourceGroupIDByNameActionResult(BaseActionResult):
28+
resource_group_id: ResourceGroupID
29+
30+
@override
31+
def entity_id(self) -> str | None:
32+
return str(self.resource_group_id)

src/ai/backend/manager/services/scaling_group/processors.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
ReplaceDefaultSessionOptionsAction,
8181
ReplaceDefaultSessionOptionsActionResult,
8282
)
83+
from ai.backend.manager.services.scaling_group.actions.resolve_resource_group_id_by_name import (
84+
ResolveResourceGroupIDByNameAction,
85+
ResolveResourceGroupIDByNameActionResult,
86+
)
8387
from ai.backend.manager.services.scaling_group.actions.update_allowed_domains_for_rg import (
8488
UpdateAllowedDomainsForResourceGroupAction,
8589
UpdateAllowedDomainsForResourceGroupActionResult,
@@ -177,6 +181,10 @@ class ScalingGroupProcessors(AbstractProcessorPackage):
177181
GetAllowedProjectsForResourceGroupAction,
178182
GetAllowedProjectsForResourceGroupActionResult,
179183
]
184+
resolve_resource_group_id_by_name: ActionProcessor[
185+
ResolveResourceGroupIDByNameAction,
186+
ResolveResourceGroupIDByNameActionResult,
187+
]
180188

181189
def __init__(
182190
self,
@@ -242,6 +250,9 @@ def __init__(
242250
self.get_allowed_projects_for_rg = ActionProcessor(
243251
service.get_allowed_projects_for_resource_group, action_monitors
244252
)
253+
self.resolve_resource_group_id_by_name = ActionProcessor(
254+
service.resolve_resource_group_id_by_name, action_monitors
255+
)
245256

246257
@override
247258
def supported_actions(self) -> list[ActionSpec]:
@@ -270,4 +281,5 @@ def supported_actions(self) -> list[ActionSpec]:
270281
GetAllowedResourceGroupsForProjectAction.spec(),
271282
GetAllowedDomainsForResourceGroupAction.spec(),
272283
GetAllowedProjectsForResourceGroupAction.spec(),
284+
ResolveResourceGroupIDByNameAction.spec(),
273285
]

0 commit comments

Comments
 (0)