Skip to content
Draft
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
26 changes: 25 additions & 1 deletion runpod/apps/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import json
import os
from abc import ABC, abstractmethod
from collections.abc import Mapping
from pathlib import Path, PurePosixPath
Expand Down Expand Up @@ -43,6 +44,7 @@ def reference(self) -> str:
@property
def path(self) -> Path:
"""the unique mount path for this volume in the current worker."""
_ensure_mounts_configured()
paths = _mount_bindings.get((self.kind, self.reference), ())
if not paths:
raise VolumeError(
Expand Down Expand Up @@ -130,6 +132,27 @@ def normalize_mounts(mounts: Optional[Mapping[str, Volume]]) -> Dict[str, Volume


_mount_bindings: Mapping[Tuple[str, str], Tuple[Path, ...]] = MappingProxyType({})
_mounts_configured = False


def _ensure_mounts_configured() -> None:
"""bind mounts from RUNPOD_MOUNTS if the worker runtime did not.

the runtime calls _configure_mounts before user code, but runtime
images released before that hook never do. deployed workers run
this vendored package, so reading the provisioning env here keeps
vol.path working regardless of the image's runtime version.
"""
if _mounts_configured:
return
raw = os.environ.get("RUNPOD_MOUNTS")
if not raw:
return
try:
bindings = json.loads(raw)
except json.JSONDecodeError as exc:
raise VolumeError("RUNPOD_MOUNTS must contain a valid JSON array") from exc
_configure_mounts(bindings)


def _configure_mounts(bindings: List[Dict[str, str]]) -> None:
Expand All @@ -153,7 +176,8 @@ def _configure_mounts(bindings: List[Dict[str, str]]) -> None:
paths = resolved.setdefault(key, [])
if path not in paths:
paths.append(path)
global _mount_bindings
global _mount_bindings, _mounts_configured
_mounts_configured = True
_mount_bindings = MappingProxyType(
{key: tuple(paths) for key, paths in resolved.items()}
)
Expand Down
42 changes: 42 additions & 0 deletions tests/test_apps/test_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,48 @@
with pytest.raises(VolumeError):
NetworkVolume("nv-1").path

def test_path_binds_from_env_when_runtime_did_not(self, monkeypatch):
# runtime images released before the mounts hook never call
# _configure_mounts; the vendored sdk reads the env itself
import runpod.apps.volume as volume_mod

monkeypatch.setattr(volume_mod, "_mounts_configured", False)
monkeypatch.setattr(volume_mod, "_mount_bindings", {})
monkeypatch.setenv(
"RUNPOD_MOUNTS",
json.dumps(
[
{
"kind": "network",
"reference": "models",
"id": "nv-1",
"path": "/runpod-volume",
}
]
),
)
assert str(NetworkVolume("models").path) == "/runpod-volume"
assert str(NetworkVolume("nv-1").path) == "/runpod-volume"

def test_runtime_configuration_wins_over_env(self, monkeypatch):
monkeypatch.setenv(
"RUNPOD_MOUNTS",
json.dumps(
[{"kind": "network", "reference": "models", "id": "nv-1", "path": "/x"}]
),
)
# clean_mounts already configured an empty set, as the runtime does
with pytest.raises(VolumeError, match="not mounted"):
NetworkVolume("models").path

def test_invalid_env_bindings_raise(self, monkeypatch):
import runpod.apps.volume as volume_mod

monkeypatch.setattr(volume_mod, "_mounts_configured", False)
monkeypatch.setenv("RUNPOD_MOUNTS", "{not json")
with pytest.raises(VolumeError, match="valid JSON"):
NetworkVolume("models").path

def test_invalid_configuration_does_not_partially_publish(self, tmp_path):
(tmp_path / "model").write_text("retained")
binding = {
Expand Down
Loading