Skip to content
Merged
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
5 changes: 4 additions & 1 deletion docs/docs/concepts/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -1102,8 +1102,11 @@ projects:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "get"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "create", "delete"]
```

Ensure you've created a ClusterRoleBinding to grant the role to the user or the service account you're using.

??? info "Resources and offers"
Expand Down
63 changes: 57 additions & 6 deletions docs/docs/reference/dstack.yml/volume.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,61 @@

The `volume` configuration type allows creating, registering, and updating [volumes](../../concepts/volumes.md).

## Root reference
=== "AWS"

#SCHEMA# dstack._internal.core.models.volumes.VolumeConfiguration
overrides:
show_root_heading: false
type:
required: true
#SCHEMA# dstack._internal.core.models.volumes.AWSVolumeConfiguration
overrides:
show_root_heading: false
backend:
required: true

=== "GCP"

#SCHEMA# dstack._internal.core.models.volumes.GCPVolumeConfiguration
overrides:
show_root_heading: false
backend:
required: true

=== "Runpod"

#SCHEMA# dstack._internal.core.models.volumes.RunpodVolumeConfiguration
overrides:
show_root_heading: false
backend:
required: true

=== "Kubernetes"

Kubernetes backend volumes are mapped to [`PersistentVolumeClaim`](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims) objects.

To create a new claim, specify `size` and optionally `storage_class_name` and/or `access_modes`:

```yaml
type: volume
backend: kubernetes
name: new-volume
size: 100GB
# By default, storage_class_name is not set, and the decision is delegated to
# the DefaultStorageClass admission controller (if it is enabled)
storage_class_name: test-nfs
# access_modes defaults to [ReadWriteOnce]. For multi-attach-capable volumes
# use ReadWriteMany and/or ReadOnlyMany
access_modes:
- ReadWriteMany
```

To reuse an existing claim, specify `claim_name`:

```yaml
type: volume
backend: kubernetes
name: existing-volume
claim_name: existing-pvc
```

#SCHEMA# dstack._internal.core.models.volumes.KubernetesVolumeConfiguration
overrides:
show_root_heading: false
backend:
required: true
6 changes: 4 additions & 2 deletions scripts/docs/gen_schema_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
from enum import Enum
from fnmatch import fnmatch
from typing import Optional

import mkdocs_gen_files
import yaml
Expand Down Expand Up @@ -85,7 +86,7 @@ def get_friendly_type(annotation: Type) -> str:

# Handle Literal — list values
if get_origin(annotation) is Literal:
values = get_args(annotation)
values = [v.value if isinstance(v, Enum) else v for v in get_args(annotation)]
return " | ".join(f'"{v}"' for v in values)

# Handle list
Expand Down Expand Up @@ -207,11 +208,12 @@ def _enrich_type_from_schema(friendly_type: str, prop_schema: Dict[str, Any]) ->
def generate_schema_reference(
model_path: str,
*,
overrides: Dict[str, Dict[str, Any]] = None,
overrides: Optional[dict[str, dict[str, Any]]] = None,
prefix: str = "",
) -> str:
module, model_name = model_path.rsplit(".", maxsplit=1)
cls = getattr(importlib.import_module(module), model_name)
assert issubclass(cls, BaseModel)
rows = []
if (
not overrides
Expand Down
16 changes: 9 additions & 7 deletions src/dstack/_internal/cli/services/configurators/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
from dstack._internal.core.errors import ResourceNotExistsError
from dstack._internal.core.models.configurations import ApplyConfigurationType
from dstack._internal.core.models.volumes import (
AnyVolumeConfiguration,
Volume,
VolumeConfiguration,
VolumeConfigurationWithRegion,
VolumePlan,
VolumeSpec,
VolumeStatus,
Expand All @@ -24,12 +25,12 @@
from dstack.api._public import Client


class VolumeConfigurator(BaseApplyConfigurator[VolumeConfiguration]):
class VolumeConfigurator(BaseApplyConfigurator[AnyVolumeConfiguration]):
TYPE = ApplyConfigurationType.VOLUME

def apply_configuration(
self,
conf: VolumeConfiguration,
conf: AnyVolumeConfiguration,
configuration_path: str,
command_args: argparse.Namespace,
configurator_args: argparse.Namespace,
Expand Down Expand Up @@ -129,7 +130,7 @@ def apply_configuration(

def delete_configuration(
self,
conf: VolumeConfiguration,
conf: AnyVolumeConfiguration,
configuration_path: str,
command_args: argparse.Namespace,
):
Expand Down Expand Up @@ -165,7 +166,7 @@ def register_args(cls, parser: argparse.ArgumentParser):
help="The volume name",
)

def apply_args(self, conf: VolumeConfiguration, args: argparse.Namespace):
def apply_args(self, conf: AnyVolumeConfiguration, args: argparse.Namespace):
if args.name:
conf.name = args.name

Expand Down Expand Up @@ -206,12 +207,13 @@ def th(s: str) -> str:
size = "-"
if plan.spec.configuration.size is not None:
size = str(plan.spec.configuration.size)
if plan.spec.configuration.volume_id is not None:
if plan.spec.configuration.is_external:
volume_type = "external"

configuration_table.add_row(th("Volume type"), volume_type)
configuration_table.add_row(th("Backend"), plan.spec.configuration.backend.value)
configuration_table.add_row(th("Region"), plan.spec.configuration.region)
if isinstance(plan.spec.configuration, VolumeConfigurationWithRegion):
configuration_table.add_row(th("Region"), plan.spec.configuration.region)
configuration_table.add_row(th("Size"), size)

console.print(configuration_table)
Expand Down
7 changes: 6 additions & 1 deletion src/dstack/_internal/cli/utils/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,12 @@ def th(s: str) -> str:
instance = offer.instance.name
if offer.total_blocks > 1:
instance += f" ({offer.blocks}/{offer.total_blocks})"
offer_backend = offer.backend.replace("remote", "ssh")
if offer.region:
offer_backend = f"{offer_backend} ({offer.region})"
offers.add_row(
f"{i}",
offer.backend.replace("remote", "ssh") + " (" + offer.region + ")",
offer_backend,
r.pretty_format(include_spot=True),
instance,
f"${offer.price:.4f}".rstrip("0").rstrip("."),
Expand Down Expand Up @@ -394,6 +397,8 @@ def _format_backend(backend_type: BackendType, region: str) -> str:
backend_str = backend_type.value
if backend_type == BackendType.REMOTE:
backend_str = "ssh"
if not region:
return backend_str
return f"{backend_str} ({region})"


Expand Down
19 changes: 10 additions & 9 deletions src/dstack/_internal/cli/utils/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ def get_volumes_table(
table.add_column("ERROR")

for volume in volumes:
backend = f"{volume.configuration.backend.value} ({volume.configuration.region})"
region = volume.configuration.region
backend = volume.get_backend().value
region = volume.get_region()
if verbose:
backend = volume.configuration.backend.value
if (
verbose
and volume.provisioning_data is not None
and volume.provisioning_data.availability_zone is not None
):
region += f" ({volume.provisioning_data.availability_zone})"
# In verbose mode, BACKEND displays `backend` only, and REGION displays nothing or
# `region` or `region (az)`
if availability_zone := volume.get_availability_zone():
region = f"{region} ({availability_zone})"
elif region:
# In non-verbose mode, BACKEND displays `backend` or `backend (region)`, and REGION
# is hidden
backend = f"{backend} ({region})"
attached = "-"
if volume.attachments is not None:
attached = ", ".join(
Expand Down
7 changes: 7 additions & 0 deletions src/dstack/_internal/core/backends/aws/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from dstack._internal.core.models.resources import Memory, Range
from dstack._internal.core.models.runs import JobProvisioningData, Requirements
from dstack._internal.core.models.volumes import (
AWSVolumeConfiguration,
Volume,
VolumeAttachmentData,
VolumeProvisioningData,
Expand Down Expand Up @@ -688,6 +689,7 @@ def terminate_gateway(
logger.debug("Deleted ALB resources for gateway %s", configuration.instance_name)

def register_volume(self, volume: Volume) -> VolumeProvisioningData:
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

logger.debug("Requesting EBS volume %s", volume.configuration.volume_id)
Expand Down Expand Up @@ -715,6 +717,7 @@ def register_volume(self, volume: Volume) -> VolumeProvisioningData:
)

def create_volume(self, volume: Volume) -> VolumeProvisioningData:
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

volume_name = generate_unique_volume_name(volume)
Expand Down Expand Up @@ -773,6 +776,7 @@ def create_volume(self, volume: Volume) -> VolumeProvisioningData:
)

def delete_volume(self, volume: Volume):
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

logger.debug("Deleting EBS volume %s", volume.configuration.name)
Expand All @@ -788,6 +792,7 @@ def delete_volume(self, volume: Volume):
def attach_volume(
self, volume: Volume, provisioning_data: JobProvisioningData
) -> VolumeAttachmentData:
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

instance_id = provisioning_data.instance_id
Expand Down Expand Up @@ -826,6 +831,7 @@ def attach_volume(
def detach_volume(
self, volume: Volume, provisioning_data: JobProvisioningData, force: bool = False
):
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

instance_id = provisioning_data.instance_id
Expand All @@ -848,6 +854,7 @@ def detach_volume(
logger.debug("Detached EBS volume %s from instance %s", volume.volume_id, instance_id)

def is_volume_detached(self, volume: Volume, provisioning_data: JobProvisioningData) -> bool:
assert isinstance(volume.configuration, AWSVolumeConfiguration)
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)

instance_id = provisioning_data.instance_id
Expand Down
3 changes: 3 additions & 0 deletions src/dstack/_internal/core/backends/gcp/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from dstack._internal.core.models.resources import Memory, Range
from dstack._internal.core.models.runs import JobProvisioningData, Requirements
from dstack._internal.core.models.volumes import (
GCPVolumeConfiguration,
Volume,
VolumeAttachmentData,
VolumeProvisioningData,
Expand Down Expand Up @@ -645,6 +646,7 @@ def terminate_gateway(
)

def register_volume(self, volume: Volume) -> VolumeProvisioningData:
assert isinstance(volume.configuration, GCPVolumeConfiguration)
logger.debug("Requesting persistent disk %s", volume.configuration.volume_id)
zones = gcp_resources.get_availability_zones(
regions_client=self.regions_client,
Expand Down Expand Up @@ -676,6 +678,7 @@ def register_volume(self, volume: Volume) -> VolumeProvisioningData:
raise ComputeError(f"Persistent disk {volume.configuration.volume_id} not found")

def create_volume(self, volume: Volume) -> VolumeProvisioningData:
assert isinstance(volume.configuration, GCPVolumeConfiguration)
zones = gcp_resources.get_availability_zones(
regions_client=self.regions_client,
project_id=self.config.project_id,
Expand Down
Loading
Loading