Fix NFS backup destinations broken by the 0.40.0 refactor (0.40.1)

_ensure_helper_image moved to stack_assets_service, but backup_destination_service
imports it lazily inside _nfs_run/_nfs_helper, so nothing failed at import time —
every NFS destination operation raised ImportError at runtime instead. The helper
is now a public ensure_helper_image() and the NFS helpers import it from its new
home. Verified: every services/ and routers/ module imports, and both NFS helper
paths run through to a Docker call instead of ImportError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-08-16 18:29:10 +00:00
co-authored by Claude Opus 5
parent 5347a36eaf
commit 4c158e9407
4 changed files with 12 additions and 12 deletions
@@ -288,10 +288,10 @@ def _nfs_run(volume: str, command: list[str]) -> str:
from config import settings
from docker_client import DockerError, get_client
from services.backup_service import _ensure_helper_image
from services.stack_assets_service import ensure_helper_image
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
try:
out = client.containers.run(
settings.BACKUP_HELPER_IMAGE,
@@ -314,10 +314,10 @@ def _nfs_helper(volume: str):
from config import settings
from docker_client import DockerError, get_client, safe_call
from services.backup_service import _ensure_helper_image
from services.stack_assets_service import ensure_helper_image
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
try:
return safe_call(
client.containers.create,
+6 -6
View File
@@ -64,7 +64,7 @@ class AssetError(Exception):
# --------------------------------------------------------------------------- #
def _ensure_helper_image(client) -> None:
def ensure_helper_image(client) -> None:
image = settings.BACKUP_HELPER_IMAGE
try:
safe_call(client.images.get, image)
@@ -104,7 +104,7 @@ def inspect_paths(paths: list[str]) -> dict[str, dict]:
if not unique:
return {}
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
mounts = {p: {"bind": f"/m/{i}", "mode": "ro"} for i, p in enumerate(unique)}
script_parts = []
for i in range(len(unique)):
@@ -149,7 +149,7 @@ def inspect_paths(paths: list[str]) -> dict[str, dict]:
def export_path(source: str, kind: str, dest_file: str) -> int:
"""Tar a host path (dir contents, or a single file) into ``dest_file``."""
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
if kind == "file":
parent, base = _split(source)
if not base:
@@ -176,7 +176,7 @@ def export_path(source: str, kind: str, dest_file: str) -> int:
def import_path(source: str, kind: str, src_file: str) -> None:
"""Unpack an archive produced by :func:`export_path` back to its host path."""
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
if kind == "file":
parent, _base = _split(source)
container = _create_helper(client, {parent: {"bind": "/dst", "mode": "rw"}})
@@ -192,7 +192,7 @@ def import_path(source: str, kind: str, src_file: str) -> None:
def export_volume(full_name: str, dest_file: str) -> int:
"""Stream a named volume's contents into ``dest_file`` (never into RAM)."""
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
container = _create_helper(client, {full_name: {"bind": "/v", "mode": "ro"}})
written = 0
try:
@@ -209,7 +209,7 @@ def export_volume(full_name: str, dest_file: str) -> int:
def import_volume(full_name: str, labels: dict, src_file: str, wipe: bool = True) -> None:
"""Restore a volume from an archive, optionally clearing it first."""
client = get_client()
_ensure_helper_image(client)
ensure_helper_image(client)
existed = True
try:
safe_call(client.volumes.get, full_name)
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the StackPilot release version."""
APP_VERSION = "0.40.0"
APP_VERSION = "0.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stackpilot-frontend",
"private": true,
"version": "0.40.0",
"version": "0.40.1",
"type": "module",
"scripts": {
"dev": "vite",