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
28 changes: 27 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."""
_bind_mounts_from_env()
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 _bind_mounts_from_env() -> None:
"""bind mounts from RUNPOD_MOUNTS unless the worker runtime already did.

the runtime calls _configure_mounts before user code, but images
released before that hook never do. deployed workers run this
vendored package, so reading the provisioning env here keeps
vol.path working on those images. no-op without the env.
"""
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,10 +176,13 @@ 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
# publish the bindings before the flag: a thread that sees the flag
# must not read the previous, empty bindings
_mount_bindings = MappingProxyType(
{key: tuple(paths) for key, paths in resolved.items()}
)
_mounts_configured = True


class VolumeResolver:
Expand Down
38 changes: 38 additions & 0 deletions tests/test_apps/test_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,44 @@ def test_multiple_bindings_require_explicit_path(self):
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
monkeypatch.setattr("runpod.apps.volume._mounts_configured", False)
monkeypatch.setattr("runpod.apps.volume._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_malformed_mounts_env_raises(self, monkeypatch):
monkeypatch.setattr("runpod.apps.volume._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