diff --git a/changes/1831.feature.md b/changes/1831.feature.md new file mode 100644 index 0000000000..d8536419cb --- /dev/null +++ b/changes/1831.feature.md @@ -0,0 +1 @@ +Introduce `endpoint.created_user_email` and `endpoint.session_owner_email` GQL field diff --git a/src/ai/backend/manager/api/admin.py b/src/ai/backend/manager/api/admin.py index 9f9d24af99..002baddc01 100644 --- a/src/ai/backend/manager/api/admin.py +++ b/src/ai/backend/manager/api/admin.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +import traceback from typing import TYPE_CHECKING, Any, Iterable, Tuple import aiohttp_cors @@ -96,6 +97,7 @@ async def _handle_gql_common(request: web.Request, params: Any) -> ExecutionResu else: errmsg = {"message": str(e)} log.error("ADMIN.GQL Exception: {}", errmsg) + log.debug("{}", "".join(traceback.format_exception(e))) return result diff --git a/src/ai/backend/manager/models/endpoint.py b/src/ai/backend/manager/models/endpoint.py index 940b90a44a..744d915397 100644 --- a/src/ai/backend/manager/models/endpoint.py +++ b/src/ai/backend/manager/models/endpoint.py @@ -147,6 +147,12 @@ class EndpointRow(Base): routings = relationship("RoutingRow", back_populates="endpoint_row") tokens = relationship("EndpointTokenRow", back_populates="endpoint_row") image_row = relationship("ImageRow", back_populates="endpoints") + created_user_row = relationship( + "UserRow", back_populates="created_endpoints", foreign_keys="EndpointRow.created_user" + ) + session_owner_row = relationship( + "UserRow", back_populates="owned_endpoints", foreign_keys="EndpointRow.session_owner" + ) def __init__( self, @@ -204,6 +210,8 @@ async def get( load_routes=False, load_tokens=False, load_image=False, + load_created_user=False, + load_session_owner=False, ) -> "EndpointRow": """ :raises: sqlalchemy.orm.exc.NoResultFound @@ -215,6 +223,10 @@ async def get( query = query.options(selectinload(EndpointRow.tokens)) if load_image: query = query.options(selectinload(EndpointRow.image_row)) + if load_created_user: + query = query.options(selectinload(EndpointRow.created_user_row)) + if load_session_owner: + query = query.options(selectinload(EndpointRow.session_owner_row)) if project: query = query.filter(EndpointRow.project == project) if domain: @@ -237,6 +249,8 @@ async def list( load_routes=False, load_image=False, load_tokens=False, + load_created_user=False, + load_session_owner=False, status_filter=[EndpointLifecycle.CREATED], ) -> List["EndpointRow"]: query = ( @@ -250,6 +264,10 @@ async def list( query = query.options(selectinload(EndpointRow.tokens)) if load_image: query = query.options(selectinload(EndpointRow.image_row)) + if load_created_user: + query = query.options(selectinload(EndpointRow.created_user_row)) + if load_session_owner: + query = query.options(selectinload(EndpointRow.session_owner_row)) if project: query = query.filter(EndpointRow.project == project) if domain: @@ -368,8 +386,16 @@ class Meta: url = graphene.String() model = graphene.UUID() model_mount_destiation = graphene.String() - created_user = graphene.UUID() - session_owner = graphene.UUID() + created_user = graphene.UUID( + deprecation_reason="Deprecated since 23.09.8; use `created_user_id`" + ) + created_user_email = graphene.String(description="Added at 23.09.8") + created_user_id = graphene.UUID(description="Added at 23.09.8") + session_owner = graphene.UUID( + deprecation_reason="Deprecated since 23.09.8; use `session_owner_id`" + ) + session_owner_email = graphene.String(description="Added at 23.09.8") + session_owner_id = graphene.UUID(description="Added at 23.09.8") tag = graphene.String() startup_command = graphene.String() bootstrap_script = graphene.String() @@ -410,7 +436,11 @@ async def from_row( model=row.model, model_mount_destiation=row.model_mount_destiation, created_user=row.created_user, + created_user_id=row.created_user, + created_user_email=row.created_user_row.email, session_owner=row.session_owner, + session_owner_id=row.session_owner, + session_owner_email=row.session_owner_row.email, tag=row.tag, startup_command=row.startup_command, bootstrap_script=row.bootstrap_script, @@ -477,6 +507,8 @@ async def load_slice( .offset(offset) .options(selectinload(EndpointRow.image_row)) .options(selectinload(EndpointRow.routings)) + .options(selectinload(EndpointRow.created_user_row)) + .options(selectinload(EndpointRow.session_owner_row)) .order_by(sa.desc(EndpointRow.created_at)) .filter( EndpointRow.lifecycle_stage.in_([ @@ -514,9 +546,15 @@ async def load_all( ) -> Sequence["Endpoint"]: async with ctx.db.begin_readonly_session() as session: rows = await EndpointRow.list( - session, project=project, domain=domain_name, user_uuid=user_uuid, load_image=True + session, + project=project, + domain=domain_name, + user_uuid=user_uuid, + load_image=True, + load_created_user=True, + load_session_owner=True, ) - return [await Endpoint.from_row(ctx, row) for row in rows] + return [await Endpoint.from_row(ctx, row) for row in rows] @classmethod async def load_item( @@ -541,10 +579,12 @@ async def load_item( project=project, load_image=True, load_routes=True, + load_created_user=True, + load_session_owner=True, ) + return await Endpoint.from_row(ctx, row) except NoResultFound: raise EndpointNotFound - return await Endpoint.from_row(ctx, row) async def resolve_status(self, info: graphene.ResolveInfo) -> str: if self.retries > SERVICE_MAX_RETRIES: diff --git a/src/ai/backend/manager/models/user.py b/src/ai/backend/manager/models/user.py index 5895c0908e..f72df26183 100644 --- a/src/ai/backend/manager/models/user.py +++ b/src/ai/backend/manager/models/user.py @@ -174,6 +174,13 @@ class UserRow(Base): resource_policy_row = relationship("UserResourcePolicyRow", back_populates="users") keypairs = relationship("KeyPairRow", back_populates="user_row", foreign_keys="KeyPairRow.user") + created_endpoints = relationship( + "EndpointRow", back_populates="created_user_row", foreign_keys="EndpointRow.created_user" + ) + owned_endpoints = relationship( + "EndpointRow", back_populates="session_owner_row", foreign_keys="EndpointRow.session_owner" + ) + main_keypair = relationship("KeyPairRow", foreign_keys=users.c.main_access_key)