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/7057.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `artifact_storages` common table for storage metadata management across object storage and VFS storage backends, and add `adminUpdateArtifactStorage` GraphQL mutation for updating artifact storage metadata (e.g., name)
51 changes: 49 additions & 2 deletions docs/manager/graphql-reference/supergraph.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,31 @@ type ArtifactStatusChangedPayload
artifactRevision: ArtifactRevision!
}

"""Added in 26.3.0. Artifact storage metadata"""
type ArtifactStorage
@join__type(graph: STRAWBERRY)
{
"""The ID of the artifact storage"""
id: ID!

"""The name of the artifact storage"""
name: String!

"""The type of the artifact storage"""
type: ArtifactStorageType!
}

"""
Added in 26.3.0. The type of artifact storage backend. OBJECT_STORAGE: Object storage (e.g., S3-compatible). VFS_STORAGE: Virtual folder storage. GIT_LFS: Git LFS storage.
"""
enum ArtifactStorageType
@join__type(graph: STRAWBERRY)
{
OBJECT_STORAGE @join__enumValue(graph: STRAWBERRY)
VFS_STORAGE @join__enumValue(graph: STRAWBERRY)
GIT_LFS @join__enumValue(graph: STRAWBERRY)
}

enum ArtifactType
@join__type(graph: STRAWBERRY)
{
Expand Down Expand Up @@ -7298,6 +7323,11 @@ type Mutation
"""Added in 25.16.0. Delete a VFS storage"""
deleteVFSStorage(input: DeleteVFSStorageInput!): DeleteVFSStoragePayload! @join__field(graph: STRAWBERRY)

"""
Added in 26.3.0. Update artifact storage metadata (common fields like name)
"""
adminUpdateArtifactStorage(input: UpdateArtifactStorageInput!): UpdateArtifactStoragePayload! @join__field(graph: STRAWBERRY)

"""
Added in 25.15.0.

Expand Down Expand Up @@ -11844,6 +11874,25 @@ type UpdateArtifactPayload
artifact: Artifact!
}

"""Added in 26.3.0. Input for updating artifact storage metadata"""
input UpdateArtifactStorageInput
@join__type(graph: STRAWBERRY)
{
"""The ID of the artifact storage"""
id: ID!

"""The new name for the artifact storage"""
name: String
}

"""Added in 26.3.0. Payload for updating artifact storage metadata"""
type UpdateArtifactStoragePayload
@join__type(graph: STRAWBERRY)
{
"""The updated artifact storage"""
artifactStorage: ArtifactStorage!
}

input UpdateAutoScalingRuleInput
@join__type(graph: STRAWBERRY)
{
Expand Down Expand Up @@ -11951,7 +12000,6 @@ input UpdateObjectStorageInput
@join__type(graph: STRAWBERRY)
{
id: ID!
name: String
host: String
accessKey: String
secretKey: String
Expand Down Expand Up @@ -12167,7 +12215,6 @@ input UpdateVFSStorageInput
@join__type(graph: STRAWBERRY)
{
id: ID!
name: String
host: String
basePath: String
}
Expand Down
43 changes: 41 additions & 2 deletions docs/manager/graphql-reference/v2-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,27 @@ type ArtifactStatusChangedPayload {
artifactRevision: ArtifactRevision!
}

"""Added in 26.3.0. Artifact storage metadata"""
type ArtifactStorage {
"""The ID of the artifact storage"""
id: ID!

"""The name of the artifact storage"""
name: String!

"""The type of the artifact storage"""
type: ArtifactStorageType!
}

"""
Added in 26.3.0. The type of artifact storage backend. OBJECT_STORAGE: Object storage (e.g., S3-compatible). VFS_STORAGE: Virtual folder storage. GIT_LFS: Git LFS storage.
"""
enum ArtifactStorageType {
OBJECT_STORAGE
VFS_STORAGE
GIT_LFS
}

enum ArtifactType {
MODEL
PACKAGE
Expand Down Expand Up @@ -3731,6 +3752,11 @@ type Mutation {
"""Added in 25.16.0. Delete a VFS storage"""
deleteVFSStorage(input: DeleteVFSStorageInput!): DeleteVFSStoragePayload!

"""
Added in 26.3.0. Update artifact storage metadata (common fields like name)
"""
adminUpdateArtifactStorage(input: UpdateArtifactStorageInput!): UpdateArtifactStoragePayload!

"""
Added in 25.15.0.

Expand Down Expand Up @@ -6942,6 +6968,21 @@ type UpdateArtifactPayload {
artifact: Artifact!
}

"""Added in 26.3.0. Input for updating artifact storage metadata"""
input UpdateArtifactStorageInput {
"""The ID of the artifact storage"""
id: ID!

"""The new name for the artifact storage"""
name: String
}

"""Added in 26.3.0. Payload for updating artifact storage metadata"""
type UpdateArtifactStoragePayload {
"""The updated artifact storage"""
artifactStorage: ArtifactStorage!
}

input UpdateAutoScalingRuleInput {
id: ID!
metricSource: AutoScalingMetricSource
Expand Down Expand Up @@ -7019,7 +7060,6 @@ type UpdateNotificationRulePayload {
"""Added in 25.14.0"""
input UpdateObjectStorageInput {
id: ID!
name: String
host: String
accessKey: String
secretKey: String
Expand Down Expand Up @@ -7209,7 +7249,6 @@ type UpdateUserV2Payload {
"""Added in 25.16.0. Input for updating VFS storage"""
input UpdateVFSStorageInput {
id: ID!
name: String
host: String
basePath: String
}
Expand Down
1 change: 1 addition & 0 deletions src/ai/backend/common/data/permission/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class EntityType(enum.StrEnum):
NETWORK = "network"
NOTIFICATION = "notification"
OBJECT_PERMISSION = "object_permission"
ARTIFACT_STORAGE = "artifact_storage"
OBJECT_STORAGE = "object_storage"
PERMISSION = "permission"
AGENT_RESOURCE = "agent_resource"
Expand Down
21 changes: 21 additions & 0 deletions src/ai/backend/common/data/storage/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from aiohttp import web

from ai.backend.common.exception import (
BackendAIError,
ErrorCode,
ErrorDetail,
ErrorDomain,
ErrorOperation,
)


class ArtifactStorageNotFoundError(BackendAIError, web.HTTPNotFound):
error_type = "https://api.backend.ai/probs/artifact-storage-not-found"
error_title = "Artifact Storage Not Found"

def error_code(self) -> ErrorCode:
return ErrorCode(
domain=ErrorDomain.ARTIFACT_STORAGE,
operation=ErrorOperation.READ,
error_detail=ErrorDetail.NOT_FOUND,
)
11 changes: 11 additions & 0 deletions src/ai/backend/common/data/storage/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

import enum
from dataclasses import dataclass

from pydantic import BaseModel, ConfigDict

from ai.backend.common.type_adapters import VFolderIDField
from ai.backend.common.types import ArtifactStorageId


class VFolderStorageTarget(BaseModel):
Expand All @@ -31,6 +33,15 @@ class ArtifactStorageType(enum.StrEnum):
GIT_LFS = "git_lfs"


@dataclass(frozen=True)
class ArtifactStorageData:
"""Data class for artifact storage metadata."""

id: ArtifactStorageId
name: str
type: ArtifactStorageType


class ArtifactStorageImportStep(enum.StrEnum):
DOWNLOAD = "download"
VERIFY = "verify"
Expand Down
1 change: 1 addition & 0 deletions src/ai/backend/common/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class ErrorDomain(enum.StrEnum):
ARTIFACT = "artifact"
ARTIFACT_REGISTRY = "artifact-registry"
ARTIFACT_ASSOCIATION = "artifact-association"
ARTIFACT_STORAGE = "artifact-storage"
OBJECT_STORAGE = "object-storage"
VFS_STORAGE = "vfs-storage"
STORAGE_NAMESPACE = "storage-namespace"
Expand Down
1 change: 1 addition & 0 deletions src/ai/backend/common/metrics/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ class LayerType(enum.StrEnum):
AUTH_REPOSITORY = "auth_repository"
ARTIFACT_REPOSITORY = "artifact_repository"
ARTIFACT_REGISTRY_REPOSITORY = "artifact_registry_repository"
ARTIFACT_STORAGE_REPOSITORY = "artifact_storage_repository"
AUDIT_LOG_REPOSITORY = "audit_log_repository"
CONTAINER_REGISTRY_REPOSITORY = "container_registry_repository"
DEPLOYMENT_REPOSITORY = "deployment_repository"
Expand Down
2 changes: 2 additions & 0 deletions src/ai/backend/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ def check_typed_tuple(value: tuple[Any, ...], types: tuple[type, ...]) -> tuple[
RuleId = NewType("RuleId", UUID)
SessionId = NewType("SessionId", UUID)
KernelId = NewType("KernelId", UUID)
# ID of the `artifact_storages` common table (storage metadata).
ArtifactStorageId = NewType("ArtifactStorageId", UUID)
ImageAlias = NewType("ImageAlias", str)
ArchName = NewType("ArchName", str)

Expand Down
124 changes: 124 additions & 0 deletions src/ai/backend/manager/api/gql/artifact_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from __future__ import annotations

import uuid
from enum import StrEnum
from typing import Self

import strawberry
from strawberry import ID, UNSET, Info

from ai.backend.common.data.storage.types import ArtifactStorageData, ArtifactStorageType
from ai.backend.common.types import ArtifactStorageId
from ai.backend.manager.api.gql.utils import check_admin_only
from ai.backend.manager.data.artifact_storages.types import ArtifactStorageUpdaterSpec
from ai.backend.manager.models.artifact_storages import ArtifactStorageRow
from ai.backend.manager.repositories.base.updater import Updater
from ai.backend.manager.services.artifact_storage.actions.update import (
UpdateArtifactStorageAction,
)
from ai.backend.manager.types import OptionalState

from .types import StrawberryGQLContext


@strawberry.enum(
name="ArtifactStorageType",
description=(
"Added in 26.3.0. The type of artifact storage backend. "
"OBJECT_STORAGE: Object storage (e.g., S3-compatible). "
"VFS_STORAGE: Virtual folder storage. "
"GIT_LFS: Git LFS storage."
),
)
class ArtifactStorageTypeGQL(StrEnum):
"""Artifact storage type enum."""

OBJECT_STORAGE = "object_storage"
VFS_STORAGE = "vfs_storage"
GIT_LFS = "git_lfs"

@classmethod
def from_internal(cls, internal_type: ArtifactStorageType) -> ArtifactStorageTypeGQL:
"""Convert internal ArtifactStorageType to GraphQL enum."""
match internal_type:
case ArtifactStorageType.OBJECT_STORAGE:
return cls.OBJECT_STORAGE
case ArtifactStorageType.VFS_STORAGE:
return cls.VFS_STORAGE
case ArtifactStorageType.GIT_LFS:
return cls.GIT_LFS

def to_internal(self) -> ArtifactStorageType:
"""Convert GraphQL enum to internal ArtifactStorageType."""
match self:
case ArtifactStorageTypeGQL.OBJECT_STORAGE:
return ArtifactStorageType.OBJECT_STORAGE
case ArtifactStorageTypeGQL.VFS_STORAGE:
return ArtifactStorageType.VFS_STORAGE
case ArtifactStorageTypeGQL.GIT_LFS:
return ArtifactStorageType.GIT_LFS


@strawberry.type(name="ArtifactStorage", description="Added in 26.3.0. Artifact storage metadata")
class ArtifactStorageGQL:
id: ID = strawberry.field(description="The ID of the artifact storage")
name: str = strawberry.field(description="The name of the artifact storage")
type: ArtifactStorageTypeGQL = strawberry.field(description="The type of the artifact storage")

@classmethod
def from_dataclass(cls, data: ArtifactStorageData) -> Self:
return cls(
id=ID(str(data.id)),
name=data.name,
type=ArtifactStorageTypeGQL.from_internal(data.type),
)


@strawberry.input(
name="UpdateArtifactStorageInput",
description="Added in 26.3.0. Input for updating artifact storage metadata",
)
class UpdateArtifactStorageInputGQL:
"""Input for updating artifact storage metadata (common fields like name)."""

id: ID = strawberry.field(description="The ID of the artifact storage")
name: str | None = strawberry.field(
default=UNSET, description="The new name for the artifact storage"
)

def to_updater(self) -> Updater[ArtifactStorageRow]:
spec = ArtifactStorageUpdaterSpec(
name=OptionalState[str].from_graphql(self.name),
)
return Updater(spec=spec, pk_value=ArtifactStorageId(uuid.UUID(self.id)))


@strawberry.type(
name="UpdateArtifactStoragePayload",
description="Added in 26.3.0. Payload for updating artifact storage metadata",
)
class UpdateArtifactStoragePayloadGQL:
artifact_storage: ArtifactStorageGQL = strawberry.field(
description="The updated artifact storage"
)


@strawberry.mutation( # type: ignore[misc]
name="adminUpdateArtifactStorage",
description="Added in 26.3.0. Update artifact storage metadata (common fields like name)",
)
async def update_artifact_storage(
input: UpdateArtifactStorageInputGQL, info: Info[StrawberryGQLContext]
) -> UpdateArtifactStoragePayloadGQL:
check_admin_only()
processors = info.context.processors

action_result = await processors.artifact_storage.update.wait_for_complete(
UpdateArtifactStorageAction(
updater=input.to_updater(),
)
)

return UpdateArtifactStoragePayloadGQL(
artifact_storage=ArtifactStorageGQL.from_dataclass(action_result.result),
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from __future__ import annotations

import uuid
from collections.abc import Sequence

from ai.backend.common.types import ArtifactStorageId
from ai.backend.manager.data.object_storage.types import ObjectStorageData
from ai.backend.manager.repositories.base import BatchQuerier, OffsetPagination
from ai.backend.manager.repositories.object_storage.options import ObjectStorageConditions
Expand All @@ -12,7 +12,7 @@

async def load_object_storages_by_ids(
processor: ObjectStorageProcessors,
storage_ids: Sequence[uuid.UUID],
storage_ids: Sequence[ArtifactStorageId],
) -> list[ObjectStorageData | None]:
"""Batch load object storages by their IDs.

Expand Down
Loading
Loading