Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/11967.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose per-mount subpath, alias (mount destination), and permission on the legacy ComputeSession GraphQL node via a new vfolder_mount_infos field.
25 changes: 25 additions & 0 deletions docs/manager/graphql-reference/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,9 @@
service_ports: JSONString
mounts: [String]
vfolder_mounts: [String]

"""Added in 26.4.4."""
vfolder_mount_infos: [VFolderMountInfo]

Check notice on line 1305 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'vfolder_mount_infos' was added to object type 'ComputeSession'

Field 'vfolder_mount_infos' was added to object type 'ComputeSession'
occupying_slots: JSONString
occupied_slots: JSONString

Expand All @@ -1311,6 +1314,25 @@
inference_metrics: JSONString
}

"""
Added in 26.4.4.

Per-mount details for a vfolder attached to a compute session.

Unlike the ``mounts`` (vfolder names) and ``vfolder_mounts`` (vfolder UUIDs)
fields, this surfaces the individual mount configuration such as the mounted
subpath, the in-container mount destination (alias), and the granted
permission for each mount.
"""
type VFolderMountInfo {

Check notice on line 1327 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Type 'VFolderMountInfo' was added

Type 'VFolderMountInfo' was added
vfolder_id: String
name: String
subpath: String
mount_destination: String
permission: String
usage_mode: String
}

type UserInfo {
email: String
full_name: String
Expand Down Expand Up @@ -1606,6 +1628,9 @@
scaling_group: String
service_ports: JSONString
vfolder_mounts: [String]

"""Added in 26.4.4."""
vfolder_mount_infos: [VFolderMountInfo]

Check notice on line 1633 in docs/manager/graphql-reference/schema.graphql

View workflow job for this annotation

GitHub Actions / GraphQL Inspector

Field 'vfolder_mount_infos' was added to object type 'ComputeSessionNode'

Field 'vfolder_mount_infos' was added to object type 'ComputeSessionNode'
occupied_slots: JSONString
requested_slots: JSONString

Expand Down
27 changes: 27 additions & 0 deletions docs/manager/graphql-reference/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2810,6 +2810,9 @@ type ComputeSession implements Item
service_ports: JSONString
mounts: [String]
vfolder_mounts: [String]

"""Added in 26.4.4."""
vfolder_mount_infos: [VFolderMountInfo]
occupying_slots: JSONString
occupied_slots: JSONString

Expand Down Expand Up @@ -2909,6 +2912,9 @@ type ComputeSessionNode implements Node
scaling_group: String @join__field(graph: GRAPHENE)
service_ports: JSONString @join__field(graph: GRAPHENE)
vfolder_mounts: [String] @join__field(graph: GRAPHENE)

"""Added in 26.4.4."""
vfolder_mount_infos: [VFolderMountInfo] @join__field(graph: GRAPHENE)
occupied_slots: JSONString @join__field(graph: GRAPHENE)
requested_slots: JSONString @join__field(graph: GRAPHENE)

Expand Down Expand Up @@ -21039,6 +21045,27 @@ type VFolderMetadataInfo
cloneable: Boolean!
}

"""
Added in 26.4.4.

Per-mount details for a vfolder attached to a compute session.

Unlike the ``mounts`` (vfolder names) and ``vfolder_mounts`` (vfolder UUIDs)
fields, this surfaces the individual mount configuration such as the mounted
subpath, the in-container mount destination (alias), and the granted
permission for each mount.
"""
type VFolderMountInfo
@join__type(graph: GRAPHENE)
{
vfolder_id: String
name: String
subpath: String
mount_destination: String
permission: String
usage_mode: String
}

"""Added in 26.4.2. Mount permission level for a virtual folder."""
enum VFolderMountPermission
@join__type(graph: STRAWBERRY)
Expand Down
54 changes: 54 additions & 0 deletions src/ai/backend/manager/api/gql_legacy/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sqlalchemy.orm import joinedload, selectinload

from ai.backend.common import validators as tx
from ai.backend.common.data.vfolder.types import VFolderMountData
from ai.backend.common.defs.session import SESSION_PRIORITY_MAX, SESSION_PRIORITY_MIN
from ai.backend.common.exception import SessionWithInvalidStateError
from ai.backend.common.types import (
Expand Down Expand Up @@ -191,6 +192,48 @@ def parse_value(value: str) -> ComputeSessionPermission:
return ComputeSessionPermission(value)


class VFolderMountInfo(graphene.ObjectType): # type: ignore[misc]
"""
Added in 26.4.4.

Per-mount details for a vfolder attached to a compute session.

Unlike the ``mounts`` (vfolder names) and ``vfolder_mounts`` (vfolder UUIDs)
fields, this surfaces the individual mount configuration such as the mounted
subpath, the in-container mount destination (alias), and the granted
permission for each mount.
"""

vfolder_id = graphene.String()
name = graphene.String()
subpath = graphene.String() # null when mounting the vfolder root
mount_destination = graphene.String() # alias / container mount path
permission = graphene.String()
usage_mode = graphene.String()


def _vfolder_mount_info_from_mount(
mount: VFolderMount | VFolderMountData,
) -> VFolderMountInfo:
"""Map a single ``VFolderMount``/``VFolderMountData`` to a ``VFolderMountInfo``."""
subpath = str(mount.vfsubpath)
return VFolderMountInfo(
vfolder_id=str(mount.vfid.folder_id),
name=mount.name,
subpath=None if subpath == "." else subpath,
mount_destination=str(mount.kernel_path),
permission=mount.mount_perm.value,
usage_mode=mount.usage_mode.value,
)


def _vfolder_mount_infos(
mounts: Iterable[VFolderMount | VFolderMountData],
) -> list[VFolderMountInfo]:
"""Map a list of mounts to a list of ``VFolderMountInfo`` objects."""
return [_vfolder_mount_info_from_mount(mount) for mount in mounts]


@graphene_federation.key("id")
class ComputeSessionNode(graphene.ObjectType): # type: ignore[misc]
class Meta:
Expand Down Expand Up @@ -247,6 +290,10 @@ class Meta:
scaling_group = graphene.String()
service_ports = graphene.JSONString()
vfolder_mounts = graphene.List(lambda: graphene.String)
vfolder_mount_infos = graphene.List(
lambda: VFolderMountInfo,
description="Added in 26.4.4.",
)
occupied_slots = graphene.JSONString()
requested_slots = graphene.JSONString()
image_references = graphene.List(
Expand Down Expand Up @@ -378,6 +425,7 @@ def from_row(
scaling_group=row.scaling_group_name,
# TODO: Deprecate 'vfolder_mounts' and replace it with a list of VirtualFolderNodes
vfolder_mounts=[vf.vfid.folder_id for vf in row.vfolders_sorted_by_id],
vfolder_mount_infos=_vfolder_mount_infos(row.vfolders_sorted_by_id),
occupied_slots=row.occupying_slots.to_json(),
requested_slots=row.requested_slots.to_json(),
image_references=row.images,
Expand Down Expand Up @@ -441,6 +489,7 @@ def from_dataclass(
agent_ids=session_data.agent_ids,
scaling_group=session_data.scaling_group_name,
vfolder_mounts=vfolder_mounts,
vfolder_mount_infos=_vfolder_mount_infos(session_data.vfolder_mounts or []),
occupied_slots=session_data.occupying_slots,
requested_slots=session_data.requested_slots,
image_references=session_data.images,
Expand Down Expand Up @@ -1021,6 +1070,10 @@ class Meta:
service_ports = graphene.JSONString()
mounts = graphene.List(lambda: graphene.String)
vfolder_mounts = graphene.List(lambda: graphene.String)
vfolder_mount_infos = graphene.List(
lambda: VFolderMountInfo,
description="Added in 26.4.4.",
)
occupying_slots = graphene.JSONString()
occupied_slots = graphene.JSONString() # legacy
requested_slots = graphene.JSONString(description="Added in 24.03.0.")
Expand Down Expand Up @@ -1103,6 +1156,7 @@ def parse_row(cls, ctx: GraphQueryContext, row: Row[Any]) -> Mapping[str, Any]:
"mounts": [*{mount.name for mount in vfolder_mounts}],
# TODO: Deprecate 'vfolder_mounts' and replace it with a list of VirtualFolderNodes
"vfolder_mounts": [*{vf.vfid.folder_id for vf in vfolder_mounts}],
"vfolder_mount_infos": _vfolder_mount_infos(vfolder_mounts),
"occupying_slots": row.occupying_slots.to_json(),
"occupied_slots": row.occupying_slots.to_json(),
"requested_slots": row.requested_slots.to_json(),
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/manager/test_vfolder_mount_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations

import uuid
from pathlib import PurePosixPath

from ai.backend.common.data.vfolder.types import VFolderMountData
from ai.backend.common.types import (
MountPermission,
VFolderID,
VFolderMount,
VFolderUsageMode,
)
from ai.backend.manager.api.gql_legacy.session import (
VFolderMountInfo,
_vfolder_mount_info_from_mount,
_vfolder_mount_infos,
)


def _make_mount(
*,
vfsubpath: str,
kernel_path: str = "/home/work/data",
name: str = "data",
mount_perm: MountPermission = MountPermission.READ_WRITE,
usage_mode: VFolderUsageMode = VFolderUsageMode.GENERAL,
) -> VFolderMount:
folder_id = uuid.uuid4()
return VFolderMount(
name=name,
vfid=VFolderID(quota_scope_id=None, folder_id=folder_id),
vfsubpath=PurePosixPath(vfsubpath),
host_path=PurePosixPath("/vfroot/data"),
kernel_path=PurePosixPath(kernel_path),
mount_perm=mount_perm,
usage_mode=usage_mode,
)


def test_mapping_with_custom_subpath_and_destination() -> None:
mount = _make_mount(
vfsubpath="sub/dir",
kernel_path="/home/work/alias",
name="myfolder",
mount_perm=MountPermission.READ_ONLY,
)
info = _vfolder_mount_info_from_mount(mount)

assert isinstance(info, VFolderMountInfo)
assert info.vfolder_id == str(mount.vfid.folder_id)
assert info.name == "myfolder"
assert info.subpath == "sub/dir"
assert info.mount_destination == "/home/work/alias"
assert info.permission == MountPermission.READ_ONLY.value
assert info.usage_mode == VFolderUsageMode.GENERAL.value


def test_mapping_root_subpath_is_none() -> None:
mount = _make_mount(vfsubpath=".")
info = _vfolder_mount_info_from_mount(mount)

assert info.subpath is None
assert info.mount_destination == "/home/work/data"


def test_mapping_from_dataclass() -> None:
folder_id = uuid.uuid4()
data = VFolderMountData(
name="data",
vfid=VFolderID(quota_scope_id=None, folder_id=folder_id),
vfsubpath=PurePosixPath("nested"),
host_path=PurePosixPath("/vfroot/data"),
kernel_path=PurePosixPath("/home/work/data"),
mount_perm=MountPermission.READ_WRITE,
usage_mode=VFolderUsageMode.GENERAL,
)
info = _vfolder_mount_info_from_mount(data)

assert info.vfolder_id == str(folder_id)
assert info.subpath == "nested"
assert info.mount_destination == "/home/work/data"
assert info.permission == MountPermission.READ_WRITE.value


def test_mapping_list_preserves_order() -> None:
mounts = [
_make_mount(vfsubpath=".", name="first"),
_make_mount(vfsubpath="x", name="second"),
]
infos = _vfolder_mount_infos(mounts)

assert [i.name for i in infos] == ["first", "second"]
assert infos[0].subpath is None
assert infos[1].subpath == "x"
Loading