| .. | .. |
|---|
| 6 | 6 | from fastapi.responses import StreamingResponse |
|---|
| 7 | 7 | |
|---|
| 8 | 8 | from app.auth import verify_token |
|---|
| 9 | | -from app.ops_runner import _BACKUP_TIMEOUT, stream_ops |
|---|
| 9 | +from app.ops_runner import _BACKUP_TIMEOUT, stream_ops_host |
|---|
| 10 | 10 | |
|---|
| 11 | 11 | router = APIRouter() |
|---|
| 12 | 12 | |
|---|
| .. | .. |
|---|
| 22 | 22 | source: str, |
|---|
| 23 | 23 | dry_run: bool, |
|---|
| 24 | 24 | ) -> AsyncGenerator[str, None]: |
|---|
| 25 | | - """Async generator that drives the restore workflow and yields SSE events.""" |
|---|
| 25 | + """Async generator that drives the restore workflow and yields SSE events. |
|---|
| 26 | + |
|---|
| 27 | + Runs on the host via nsenter because ops restore delegates to project CLIs |
|---|
| 28 | + that use host Python venvs incompatible with the container's Python. |
|---|
| 29 | + """ |
|---|
| 26 | 30 | base_args = ["restore", project, env] |
|---|
| 27 | 31 | if dry_run: |
|---|
| 28 | 32 | base_args.append("--dry-run") |
|---|
| 29 | 33 | |
|---|
| 30 | 34 | if source == "offsite": |
|---|
| 31 | | - # ops offsite restore <project> <env> — downloads from offsite storage |
|---|
| 35 | + # ops offsite restore <project> <env> |
|---|
| 32 | 36 | download_args = ["offsite", "restore", project, env] |
|---|
| 33 | 37 | yield _sse_line({"line": f"Downloading {project}/{env} from offsite...", "timestamp": _now()}) |
|---|
| 34 | 38 | |
|---|
| 35 | 39 | download_ok = True |
|---|
| 36 | | - async for line in stream_ops(download_args, timeout=_BACKUP_TIMEOUT): |
|---|
| 40 | + async for line in stream_ops_host(download_args, timeout=_BACKUP_TIMEOUT): |
|---|
| 37 | 41 | yield _sse_line({"line": line, "timestamp": _now()}) |
|---|
| 38 | 42 | if line.startswith("[error]"): |
|---|
| 39 | 43 | download_ok = False |
|---|
| .. | .. |
|---|
| 45 | 49 | yield _sse_line({"line": "Download complete. Starting restore...", "timestamp": _now()}) |
|---|
| 46 | 50 | |
|---|
| 47 | 51 | success = True |
|---|
| 48 | | - async for line in stream_ops(base_args, timeout=_BACKUP_TIMEOUT): |
|---|
| 52 | + async for line in stream_ops_host(base_args, timeout=_BACKUP_TIMEOUT): |
|---|
| 49 | 53 | yield _sse_line({"line": line, "timestamp": _now()}) |
|---|
| 50 | 54 | if line.startswith("[error]"): |
|---|
| 51 | 55 | success = False |
|---|
| .. | .. |
|---|
| 69 | 73 | Restore a backup for the given project/env. |
|---|
| 70 | 74 | |
|---|
| 71 | 75 | Uses Server-Sent Events (SSE) to stream real-time progress. |
|---|
| 72 | | - Parameters are passed as query strings since EventSource only supports GET. |
|---|
| 76 | + Runs on the host via nsenter for Python venv compatibility. |
|---|
| 73 | 77 | """ |
|---|
| 74 | 78 | return StreamingResponse( |
|---|
| 75 | 79 | _restore_generator(project, env, source, dry_run), |
|---|