diff --git a/changes/8205.docs.md b/changes/8205.docs.md new file mode 100644 index 00000000000..c49455f0b61 --- /dev/null +++ b/changes/8205.docs.md @@ -0,0 +1 @@ +Add BEP-1036 for Artifact Storage Usage Tracking and Quota Enforcement diff --git a/proposals/BEP-1036-artifact-storage-quota.md b/proposals/BEP-1036-artifact-storage-quota.md new file mode 100644 index 00000000000..4d1130f1b6b --- /dev/null +++ b/proposals/BEP-1036-artifact-storage-quota.md @@ -0,0 +1,264 @@ +--- +Author: Gyubong Lee (gbl@lablup.com) +Status: Draft +Created: 2026-01-22 +Created-Version: 26.2.0 +Target-Version: +Implemented-Version: +--- + +# Artifact Storage Usage Tracking and Quota Enforcement + +## Motivation + +Currently, artifact storage has no usage tracking or capacity limits. When artifacts are imported, they are stored without any visibility into how much space is being consumed or any mechanism to prevent storage exhaustion. + +This creates operational risks: + +- Storage can be exhausted without warning +- No visibility into storage utilization +- No way to enforce capacity planning or cost control + +The artifact import flow supports two storage destinations, and both need quota enforcement: + +1. **Default (StorageNamespace)**: No quota system exists +2. **VFolder destination**: Quota system exists (`max_quota_scope_size`) but not integrated into artifact import pre-check + +## Current Design + +### Existing Data Model + +- **`StorageNamespaceRow`**: Tracks basic namespace information (`id`, `storage_id`, `namespace`). No quota-related fields. +- **`ArtifactRevisionRow`**: Already tracks `size` for individual revisions. +- **`AssociationArtifactsStorageRow`**: Links artifact revisions to storage namespaces. + +### Dual Storage Destination + +The artifact import flow (`import_revision()`) supports two destinations: + +| Destination | When | Current Quota | +|-------------|------|---------------| +| StorageNamespace | `vfolder_id` is None | **None** | +| VFolder | `vfolder_id` is provided | `max_quota_scope_size` in resource policy (enforced by storage proxy at write time) | + +### VFolder Quota System + +VFolders have an existing quota system: + +- `VFolderRow.quota_scope_id` → Links to user or project +- Resource policies define `max_quota_scope_size` +- Storage proxy enforces quota at filesystem write time + +**Problem**: The artifact import does not pre-check VFolder quota before starting the import. Large imports can fail mid-way when the storage proxy rejects writes due to quota limits. + +## Proposed Design + +### Overview + +Create a unified quota enforcement layer that performs pre-validation before artifact import begins, regardless of storage destination. + +### Unified Quota Service + +Create `ArtifactStorageQuotaService` that handles both storage destinations with a single entry point: + +```python +class ArtifactStorageQuotaService: + """Unified quota service for artifact storage.""" + + async def check_quota( + self, + storage_destination: StorageDestination, + additional_size: int, + ) -> None: + match storage_destination: + case StorageNamespaceDestination(namespace_id): + await self._check_storage_namespace_quota(namespace_id, additional_size) + case VFolderDestination(vfolder_id, quota_scope_id): + await self._check_vfolder_quota(vfolder_id, quota_scope_id, additional_size) +``` + +### Storage Destination Details + +Each storage destination has different quota mechanisms: + +| Destination | Quota Source | Usage Source | Details | +|-------------|-------------|--------------|---------| +| StorageNamespace | `StorageNamespaceRow.max_size` (NEW) | Aggregated from `artifact_revisions` via association table | [storage_namespace.md](BEP-1036/storage_namespace.md) | +| VFolder | `max_quota_scope_size` from resource policy | Storage proxy API | [vfolder_storage.md](BEP-1036/vfolder_storage.md) | + +### Import Flow with Quota Check + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ import_revision(action) │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Get revision_data (includes size) │ +│ │ +│ 2. Determine storage destination │ +│ ┌─────────────────────────────┬────────────────────────────────────┐ │ +│ │ vfolder_id provided? │ │ │ +│ └──────────┬──────────────────┴────────────────────┬───────────────┘ │ +│ │ YES │ NO │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ VFolderDestination │ │ StorageNamespaceDestination│ │ +│ │ - vfolder_id │ │ - namespace_id │ │ +│ │ - quota_scope_id │ │ │ │ +│ └───────────┬─────────────┘ └───────────────┬─────────────┘ │ +│ │ │ │ +│ └──────────────┬──────────────────────────┘ │ +│ ▼ │ +│ 3. ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ ArtifactStorageQuotaService.check_quota() │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────┴──────────────┐ │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ _check_vfolder_quota() │ │ _check_storage_namespace_quota()│ │ +│ └───────────┬─────────────┘ └───────────────┬─────────────────┘ │ +│ │ │ │ +│ └──────────────┬────────────────────┘ │ +│ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ Quota Exceeded? │ │ +│ └──────────────┬───────────────┘ │ +│ YES │ │ NO │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Raise appropriate error │ │ 4. Proceed with import │ │ +│ │ - VFolderQuotaExceeded │ │ - Call storage proxy │ │ +│ │ - StorageNamespace │ │ - Update status │ │ +│ │ QuotaExceeded │ │ - Associate with storage │ │ +│ └─────────────────────────┘ └─────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### Data Sources for Quota Check + +**StorageNamespace**: Usage is aggregated from artifact revisions linked via association table. + +``` +┌─────────────────────┐ ┌─────────────────────────────────┐ +│ storage_namespace │ │ association_artifacts_storages │ +├─────────────────────┤ ├─────────────────────────────────┤ +│ id │◄────│ storage_namespace_id │ +│ max_size (NEW) │ │ artifact_revision_id ───────────┼──┐ +└─────────────────────┘ └─────────────────────────────────┘ │ + │ + ┌─────────────────────────────────┐ │ + │ artifact_revisions │ │ + ├─────────────────────────────────┤ │ + │ id ◄────────────────────────────┼──┘ + │ size │ + └─────────────────────────────────┘ +``` + +**VFolder**: Usage is queried from storage proxy, limit comes from resource policies. + +``` +┌─────────────────────┐ ┌─────────────────────────────────┐ +│ vfolders │ │ user_resource_policies / │ +├─────────────────────┤ │ project_resource_policies │ +│ id │ ├─────────────────────────────────┤ +│ quota_scope_id ─────┼────►│ max_quota_scope_size │ +└─────────────────────┘ └─────────────────────────────────┘ + │ + │ VFolderID + ▼ +┌─────────────────────┐ +│ Storage Proxy │ +├─────────────────────┤ +│ get_quota_scope_ │ +│ usage(quota_scope) │──► Current usage in bytes +└─────────────────────┘ +``` + +### Quota Check Comparison + +| Aspect | StorageNamespace | VFolder | +|--------|------------------|---------| +| Limit Source | `storage_namespace.max_size` | `resource_policy.max_quota_scope_size` | +| Usage Source | DB aggregation via association table | Storage proxy API | +| Unlimited Value | `NULL` | `-1` | +| Scope | Per namespace | Per quota scope (user/project) | + +### Error Types + +```python +class StorageQuotaExceededError(BackendError): + """Base class for quota exceeded errors.""" + pass + +class StorageNamespaceQuotaExceededError(StorageQuotaExceededError): + """Raised when StorageNamespace quota would be exceeded.""" + namespace_id: uuid.UUID + current_size: int + max_size: int + requested_size: int + +class VFolderQuotaExceededError(StorageQuotaExceededError): + """Raised when VFolder quota scope limit would be exceeded.""" + vfolder_id: VFolderID + quota_scope_id: QuotaScopeID + current_size: int + max_size: int + requested_size: int +``` + +### REST API Endpoints + +#### GET /storage-namespaces/{id}/usage + +Returns storage namespace usage statistics. + +```json +{ + "namespace_id": "...", + "total_size": 10737418240, + "max_size": 107374182400, + "revision_count": 42, + "utilization_percent": 10.0 +} +``` + +#### PATCH /storage-namespaces/{id}/quota + +Updates quota for a storage namespace. Admin-only. + +```json +{ "max_size": 107374182400 } // or null for unlimited +``` + +## Testing Scenarios + +### StorageNamespace Quota Tests +- Quota check passes when under limit +- Quota check passes when `max_size` is NULL (unlimited) +- Quota check raises error when limit would be exceeded +- Import flow rejects artifact when quota exceeded + +### VFolder Quota Tests +- Pre-check queries storage proxy for current usage +- Quota check uses `max_quota_scope_size` from resource policy +- Import is rejected before starting if quota would be exceeded +- Handles case when resource policy limit is unlimited (-1) + +### Unified Flow Tests +- Correct quota system is selected based on `vfolder_id` presence +- Error messages clearly indicate which quota was exceeded + +## Future Ideas + +### Quota Threshold Notifications + +When storage usage approaches the configured limit, the system could notify administrators: + +- Threshold levels: 80%, 90%, 95% utilization +- Integration with existing notification system + +## References + +- [BEP-1019: MinIO Artifact Registry Storage](BEP-1019-minio-artifact-registry-storage.md) diff --git a/proposals/BEP-1036/storage_namespace.md b/proposals/BEP-1036/storage_namespace.md new file mode 100644 index 00000000000..28114aed51f --- /dev/null +++ b/proposals/BEP-1036/storage_namespace.md @@ -0,0 +1,137 @@ +# StorageNamespace Quota + +Quota system for default artifact storage (Reservoir archive storage). + +## Overview + +When `vfolder_id` is not provided, artifacts are stored in the configured Reservoir archive storage. +This storage is managed by `StorageNamespaceRow` and supports two backend types: + +- **Object Storage** (MinIO, S3, etc.) +- **VFS Storage** (local/network filesystem) + +Both types share the same quota mechanism via `StorageNamespaceRow.max_size`. + +## Data Model + +``` +┌─────────────────────────┐ +│ object_storage │ +├─────────────────────────┤ +│ id │◄─────┐ +│ name │ │ +│ host │ │ +│ endpoint │ │ +│ access_key │ │ +│ secret_key │ │ +└─────────────────────────┘ │ + │ storage_id (object_storage) +┌─────────────────────────┐ │ +│ storage_namespace │──────┘ +├─────────────────────────┤ +│ id │◄─────────────────────────────────────┐ +│ storage_id │ │ +│ namespace │ │ +│ max_size (NEW) │ ◄── NULL = unlimited, in bytes │ +└─────────────────────────┘ │ + │ +┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ vfs_storage │ │ association_artifacts_ │ │ +├─────────────────────────┤ │ storages │ │ +│ id │◄──┐ ├─────────────────────────┤ │ +│ name │ │ │ id │ │ +│ host │ │ │ artifact_revision_id ───┼──┐ │ +│ base_path │ │ │ storage_namespace_id ───┼──┼──┘ +└─────────────────────────┘ │ │ storage_type │ │ + │ └─────────────────────────┘ │ + │ │ │ + │ │ "object_storage" │ + │ │ or "vfs_storage" │ + │ │ │ + └─────────┘ │ + storage_id (vfs) │ + │ +┌─────────────────────────┐ │ +│ artifact_revisions │ │ +├─────────────────────────┤ │ +│ id ◄────────────────────┼───────────────────────────────────┘ +│ artifact_id │ +│ version │ +│ size │ ◄── Individual revision size +│ status │ +└─────────────────────────┘ +``` + +## Backend Type Differences + +| Aspect | Object Storage | VFS Storage | +|--------|---------------|-------------| +| Config key for namespace | `bucket_name` | `subpath` | +| Reference table | `object_storage` | `vfs_storage` | +| Physical storage | S3 bucket (e.g., `s3://artifacts/`) | Filesystem path (e.g., `/mnt/nfs/artifacts/`) | + +Quota management and usage aggregation are identical for both types. + +## Quota Check Logic + +```python +async def check_storage_namespace_quota( + self, + namespace_id: uuid.UUID, + additional_size: int, +) -> None: + """ + Check the quota for a StorageNamespace. + Same logic applies regardless of storage_type. + """ + namespace = await self._storage_namespace_repo.get_by_id(namespace_id) + + # NULL means unlimited + if namespace.max_size is None: + return + + # Aggregate current usage via association table + usage = await self._storage_namespace_repo.get_usage(namespace_id) + + if usage.total_size + additional_size > namespace.max_size: + raise StorageNamespaceQuotaExceededError(...) +``` + +### Usage Aggregation Query + +```sql +SELECT + COALESCE(SUM(ar.size), 0) as total_size, + COUNT(ar.id) as revision_count +FROM association_artifacts_storages aas +JOIN artifact_revisions ar ON aas.artifact_revision_id = ar.id +WHERE aas.storage_namespace_id = :namespace_id +``` + +## API + +### GET /storage-namespaces/{id}/usage + +```json +{ + "namespace_id": "550e8400-e29b-41d4-a716-446655440000", + "storage_type": "object_storage", + "namespace": "artifacts", + "total_size": 10737418240, + "max_size": 107374182400, + "revision_count": 42, + "utilization_percent": 10.0 +} +``` + +### PATCH /storage-namespaces/{id}/quota + +```json +{ "max_size": 107374182400 } +``` + +Or set to unlimited: + +```json +{ "max_size": null } +``` diff --git a/proposals/BEP-1036/vfolder_storage.md b/proposals/BEP-1036/vfolder_storage.md new file mode 100644 index 00000000000..ba71fa9a9e8 --- /dev/null +++ b/proposals/BEP-1036/vfolder_storage.md @@ -0,0 +1,296 @@ +# VFolder Storage Quota + +Quota system for artifacts stored in a specific VFolder when the user provides a `vfolder_id`. + +## Overview + +When `vfolder_id` is provided in `import_revision()`, the artifact is stored in the specified VFolder. +VFolders already have an existing quota system via `quota_scope`, but there is **no pre-validation** during artifact import. + +**Current Problems:** +- No quota check before import starts +- Large artifact imports can fail mid-way when storage proxy detects quota exceeded +- Failed imports may leave partial downloads + +## Data Model + +### VFolder Quota Structure + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ VFolder Quota Structure │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ quota_scope_id determination based on Ownership Type │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ User-owned VFolder │ │ Project-owned VFolder│ │ +│ ├─────────────────────┤ ├─────────────────────┤ │ +│ │ ownership_type: │ │ ownership_type: │ │ +│ │ "user" │ │ "group" │ │ +│ │ │ │ │ │ +│ │ quota_scope_id: │ │ quota_scope_id: │ │ +│ │ "user:{user_uuid}"│ │ "project:{group_id}" │ +│ └──────────┬──────────┘ └──────────┬──────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ users │ │ groups │ │ +│ ├─────────────────────┤ ├─────────────────────┤ │ +│ │ uuid │ │ id │ │ +│ │ resource_policy ────┼──┐ │ resource_policy ────┼──┐ │ +│ └─────────────────────┘ │ └─────────────────────┘ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ user_resource_policies │ │ project_resource_policies │ │ +│ ├─────────────────────────────┤ ├─────────────────────────────┤ │ +│ │ name │ │ name │ │ +│ │ max_vfolder_count │ │ max_vfolder_count │ │ +│ │ max_quota_scope_size ◄─────┼──┼── Quota limit (bytes) │ │ +│ │ ... │ │ max_network_count │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### VFolder Row Structure + +``` +┌─────────────────────────┐ +│ vfolders │ +├─────────────────────────┤ +│ id │ +│ name │ +│ host │ ─── storage proxy host (e.g., "local:volume1") +│ quota_scope_id │ ─── "user:{uuid}" or "project:{uuid}" +│ usage_mode │ +│ permission │ +│ ownership_type │ ─── "user" or "group" +│ user │ ─── owner user UUID (for user-owned) +│ group │ ─── owner group ID (for project-owned) +│ ... │ +└─────────────────────────┘ +``` + +## Import Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ VFolder Destination Import Flow │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ import_revision(action) where action.vfolder_id is provided │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 1. Get VFolder info │ │ +│ │ vfolder = await vfolder_repository.get_by_id(action.vfolder_id) │ │ +│ │ │ │ +│ │ vfolder = { │ │ +│ │ id: "...", │ │ +│ │ host: "local:volume1", │ │ +│ │ quota_scope_id: "user:abc123...", │ │ +│ │ } │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 2. Build VFolderID │ │ +│ │ vfolder_id = VFolderID(vfolder.quota_scope_id, vfolder.id) │ │ +│ │ volume_name = parse_host(vfolder.host) # "volume1" │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 3. Quota Check (NEW) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ +│ │ │ a. Get current usage from storage proxy │ │ │ +│ │ │ usage = await storage_client.get_quota_scope_usage( │ │ │ +│ │ │ volume_name, quota_scope_id │ │ │ +│ │ │ ) │ │ │ +│ │ │ # returns: { used_bytes: 5368709120 } │ │ │ +│ │ └──────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ +│ │ │ b. Get quota limit from resource policy │ │ │ +│ │ │ max_size = await get_quota_scope_limit(quota_scope_id)│ │ │ +│ │ │ # Query user_resource_policies or │ │ │ +│ │ │ # project_resource_policies based on scope type │ │ │ +│ │ │ # returns: 10737418240 (10GB) or -1 (unlimited) │ │ │ +│ │ └──────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────────────────────────┐ │ │ +│ │ │ c. Compare │ │ │ +│ │ │ if max_size > 0: # -1 means unlimited │ │ │ +│ │ │ if usage.used_bytes + revision.size > max_size: │ │ │ +│ │ │ raise VFolderQuotaExceededError(...) │ │ │ +│ │ └──────────────────────────────────────────────────────────┘ │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 4. Proceed with import │ │ +│ │ vfolder_target = VFolderStorageTarget( │ │ +│ │ vfolder_id=vfolder_id, │ │ +│ │ volume_name=volume_name, │ │ +│ │ ) │ │ +│ │ │ │ +│ │ await storage_proxy.import_huggingface_models( │ │ +│ │ storage_step_target_mappings={ │ │ +│ │ step: vfolder_target for step in ArtifactStorageImportStep │ +│ │ } │ │ +│ │ ) │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────┐ │ +│ │ 5. Storage Proxy writes to VFolder path │ │ +│ │ /mnt/vfhost/quota_scope_id/vfolder_id/{artifact_files} │ │ +│ └────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Quota Check Logic + +```python +async def check_vfolder_quota( + self, + vfolder_data: VFolderData, + additional_size: int, +) -> None: + """ + Check the quota scope limit for a VFolder. + """ + quota_scope_id = vfolder_data.quota_scope_id + _, volume_name = self._storage_manager.get_proxy_and_volume(vfolder_data.host) + + # 1. Get current quota scope usage from storage proxy + storage_client = self._storage_manager.get_manager_facing_client(vfolder_data.host) + usage = await storage_client.get_quota_scope_usage(volume_name, str(quota_scope_id)) + + # 2. Get limit from resource policy + max_size = await self._get_quota_scope_limit(quota_scope_id) + + # -1 means unlimited + if max_size < 0: + return + + # 3. Check if limit would be exceeded + if usage.used_bytes + additional_size > max_size: + raise VFolderQuotaExceededError( + vfolder_id=VFolderID(quota_scope_id, vfolder_data.id), + quota_scope_id=quota_scope_id, + current_size=usage.used_bytes, + max_size=max_size, + requested_size=additional_size, + ) + + +async def _get_quota_scope_limit(self, quota_scope_id: QuotaScopeID) -> int: + """ + Parse scope type from QuotaScopeID and query the appropriate resource policy. + """ + scope_type, scope_uuid = quota_scope_id.split(":", 1) + + match scope_type: + case "user": + user = await self._user_repo.get_by_uuid(UUID(scope_uuid)) + policy = await self._user_policy_repo.get_by_name(user.resource_policy) + return policy.max_quota_scope_size + + case "project": + group = await self._group_repo.get_by_id(UUID(scope_uuid)) + policy = await self._project_policy_repo.get_by_name(group.resource_policy) + return policy.max_quota_scope_size +``` + +## Storage Proxy API + +### GET /volumes/{volume}/quota-scopes/{quota_scope_id} + +Returns current usage of the quota scope used by the VFolder. + +**Request:** +``` +GET /volumes/volume1/quota-scopes/user:abc123... +``` + +**Response:** +```json +{ + "used_bytes": 5368709120, + "limit_bytes": 10737418240 +} +``` + +> Note: `limit_bytes` is the limit set at the storage proxy level and may be managed +> separately from the manager's resource policy. This BEP prioritizes the manager's +> resource policy value. + +## User vs Project VFolder Comparison + +| Aspect | User VFolder | Project VFolder | +|--------|-------------|-----------------| +| `ownership_type` | `"user"` | `"group"` | +| `quota_scope_id` format | `"user:{user_uuid}"` | `"project:{group_id}"` | +| Resource Policy table | `user_resource_policies` | `project_resource_policies` | +| Quota field | `max_quota_scope_size` | `max_quota_scope_size` | +| Owner reference | `vfolders.user` | `vfolders.group` | + +## Current vs Proposed Behavior + +### Current Behavior + +``` +import_revision(vfolder_id=...) + │ + ▼ +storage_proxy.import_models(vfolder_target=...) + │ + ▼ +Storage Proxy detects quota exceeded during file write + │ + ▼ +Error returned (import fails mid-way) + │ + ▼ +May leave partial download state +``` + +### Proposed Behavior + +``` +import_revision(vfolder_id=...) + │ + ▼ +quota_service.check_vfolder_quota(...) ◄── NEW: Pre-validation + │ + ├── If exceeded: Return VFolderQuotaExceededError immediately + │ + ▼ (if passed) +storage_proxy.import_models(vfolder_target=...) + │ + ▼ +Complete successfully +``` + +## Error Response Example + +```json +{ + "error": "VFolderQuotaExceededError", + "message": "VFolder quota scope limit would be exceeded", + "details": { + "vfolder_id": "user:abc123.../vf-123...", + "quota_scope_id": "user:abc123...", + "current_size_bytes": 5368709120, + "max_size_bytes": 10737418240, + "requested_size_bytes": 8589934592, + "available_bytes": 5368709120 + } +} +```