From 79c9af21faab0bea956c32c4907fb67a434a8847 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 16 Oct 2026 20:10:21 -0400 Subject: [PATCH] fix(apps): bind volume mounts from RUNPOD_MOUNTS when the runtime did not vol.path reads bindings the worker runtime installs through _configure_mounts before user code. Runtime images published before that hook never call it, so on them every deployed NetworkVolume attached but vol.path raised "not mounted in the current execution". Deployed workers run the client's vendored runpod package, so the first vol.path lookup now installs the bindings from the RUNPOD_MOUNTS provisioning env itself when the runtime has not. A runtime that configured mounts still takes precedence. --- runpod/apps/volume.py | 26 ++++++++++++++++++++- tests/test_apps/test_volume.py | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/runpod/apps/volume.py b/runpod/apps/volume.py index 943c80b5..35579842 100644 --- a/runpod/apps/volume.py +++ b/runpod/apps/volume.py @@ -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 @@ -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( @@ -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: @@ -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()} ) diff --git a/tests/test_apps/test_volume.py b/tests/test_apps/test_volume.py index 70c255db..3ad36829 100644 --- a/tests/test_apps/test_volume.py +++ b/tests/test_apps/test_volume.py @@ -109,6 +109,48 @@ 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 + 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 = {