From 35bf7ffe514060e15dc10530cc6aec36ad1eb168 Mon Sep 17 00:00:00 2001 From: Dmitry Meyer Date: Wed, 29 Apr 2026 15:33:03 +0000 Subject: [PATCH] Kubernetes: add `read_only` volume property If set to `true`, enforces `readOnly: true` in `volumeMounts[]` Closes: https://github.com/dstackai/dstack/issues/3837 --- scripts/docs/gen_schema_reference.py | 18 +++++++++++++++--- .../core/backends/kubernetes/compute.py | 4 +++- .../_internal/core/compatibility/volumes.py | 12 +++++++----- src/dstack/_internal/core/models/volumes.py | 3 +++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/scripts/docs/gen_schema_reference.py b/scripts/docs/gen_schema_reference.py index f141200cc7..2df30c845d 100644 --- a/scripts/docs/gen_schema_reference.py +++ b/scripts/docs/gen_schema_reference.py @@ -4,6 +4,7 @@ import importlib import inspect +import json import logging import re from enum import Enum @@ -233,15 +234,26 @@ def generate_schema_reference( schema_props = {} for name, field in cls.__fields__.items(): default = field.default - if isinstance(default, Enum): - default = default.value + default_repr: Optional[str] + if default is None: + default_repr = None + elif isinstance(default, (list, tuple, dict)) and len(default) == 0: + default_repr = None + elif isinstance(default, str): + default_repr = default + elif isinstance(default, BaseModel): + default_repr = str(default) + elif isinstance(default, Enum): + default_repr = str(default.value) + else: + default_repr = json.dumps(default) friendly_type = get_friendly_type(field.annotation) friendly_type = _enrich_type_from_schema(friendly_type, schema_props.get(name, {})) values = dict( name=name, description=field.field_info.description, type=friendly_type, - default=default, + default=default_repr, required=field.required, ) # TODO: If the field doesn't have description (e.g. BaseConfiguration.type), we could fallback to docstring diff --git a/src/dstack/_internal/core/backends/kubernetes/compute.py b/src/dstack/_internal/core/backends/kubernetes/compute.py index 4ea833bd08..3338747b3d 100644 --- a/src/dstack/_internal/core/backends/kubernetes/compute.py +++ b/src/dstack/_internal/core/backends/kubernetes/compute.py @@ -255,6 +255,7 @@ def run_job( else: assert False, f"unexpected mount point: {mount_point!r}" for volume in volumes: + assert isinstance(volume.configuration, KubernetesVolumeConfiguration) pvc_name = volume.volume_id assert pvc_name is not None, f"missing PVC name: {volume!r}" mount_path = volume_name_path_map.get(volume.name) @@ -265,7 +266,6 @@ def run_job( name=volume_name, persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource( claim_name=pvc_name, - read_only=False, ), ), ) @@ -273,6 +273,8 @@ def run_job( client.V1VolumeMount( name=volume_name, mount_path=mount_path, + read_only=volume.configuration.read_only, + recursive_read_only="IfPossible" if volume.configuration.read_only else None, ) ) diff --git a/src/dstack/_internal/core/compatibility/volumes.py b/src/dstack/_internal/core/compatibility/volumes.py index c66819724e..44ee882511 100644 --- a/src/dstack/_internal/core/compatibility/volumes.py +++ b/src/dstack/_internal/core/compatibility/volumes.py @@ -1,5 +1,9 @@ from dstack._internal.core.models.common import IncludeExcludeDictType -from dstack._internal.core.models.volumes import AnyVolumeConfiguration, VolumeSpec +from dstack._internal.core.models.volumes import ( + AnyVolumeConfiguration, + KubernetesVolumeConfiguration, + VolumeSpec, +) def get_volume_spec_excludes(volume_spec: VolumeSpec) -> IncludeExcludeDictType: @@ -29,9 +33,7 @@ def _get_volume_configuration_excludes( ) -> IncludeExcludeDictType: configuration_excludes: IncludeExcludeDictType = {} - # Add excludes like this: - # - # if configuration.tags is None: - # configuration_excludes["tags"] = True + if isinstance(configuration, KubernetesVolumeConfiguration) and not configuration.read_only: + configuration_excludes["read_only"] = True return configuration_excludes diff --git a/src/dstack/_internal/core/models/volumes.py b/src/dstack/_internal/core/models/volumes.py index fbbd0b1555..03d52d2ac9 100644 --- a/src/dstack/_internal/core/models/volumes.py +++ b/src/dstack/_internal/core/models/volumes.py @@ -165,6 +165,9 @@ class KubernetesVolumeConfiguration(BaseVolumeConfiguration): list[str], Field(description="A list of accepted access modes. Ignored if `claim_name` is set"), ] = ["ReadWriteOnce"] + read_only: Annotated[ + bool, Field(description="If `true`, enforces the volume to be mounted as read-only") + ] = False @property def external_volume_id(self) -> Optional[str]: