Phase 8: back up & restore remote (agent) stacks (0.8.0)
- Agent: GET /agent/stacks/{id}/backup + POST /agent/stacks/restore (reuse
backup_service). backup_service gains backup_basename/backup_filename helpers.
- Main proxy streams agent <-> main <-> destination (creds stay central):
agent_service download_to_file/upload_file; routers/agents.py backup download,
backup/push, restore upload, restore-from.
- Schedules: BackupSchedule.agent_id; schedule_service downloads from the agent
when set; per-host filename prefix isolates retention across hosts.
- Frontend: agents api backup/restore; BackupButton/RestoreButton agent-aware
(Backup on remote stack detail, Restore per host section); schedule form host
selector (local or an online agent) + host shown on schedule rows.
Rough-verified (per request): py_compile, frontend tsc build, image imports
(main 99 / agent 16 routes). Full live agent round-trip to be tested post-deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
84ef3df59e
commit
5cd55382ed
+52
-4
@@ -14,17 +14,19 @@ import logging
|
||||
import os
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import tempfile
|
||||
|
||||
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import settings
|
||||
from docker_client import DockerError, get_client, safe_call
|
||||
from services import compose_service
|
||||
from services import backup_service, compose_service
|
||||
|
||||
logger = logging.getLogger("stackpilot.agent")
|
||||
|
||||
AGENT_VERSION = "0.5.0"
|
||||
AGENT_VERSION = "0.8.0"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -226,6 +228,52 @@ async def stack_logs(stack_id: str, tail: int = Query(200, le=2000)) -> dict:
|
||||
return {"logs": result.get("stdout", "") + result.get("stderr", "")}
|
||||
|
||||
|
||||
@app.get("/agent/stacks/{stack_id}/backup", dependencies=[Depends(verify_token)])
|
||||
async def backup_stack(
|
||||
stack_id: str,
|
||||
include_volumes: bool = Query(True),
|
||||
stop_first: bool = Query(True),
|
||||
):
|
||||
if not os.path.isdir(compose_service.stack_dir(stack_id)):
|
||||
raise HTTPException(status_code=404, detail=f"Stack '{stack_id}' not found")
|
||||
try:
|
||||
path = await backup_service.create_backup(
|
||||
stack_id, stack_id, include_volumes=include_volumes, stop_first=stop_first,
|
||||
)
|
||||
except backup_service.BackupError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return FileResponse(
|
||||
path,
|
||||
media_type="application/gzip",
|
||||
filename=backup_service.backup_filename(stack_id, include_volumes),
|
||||
)
|
||||
|
||||
|
||||
@app.post("/agent/stacks/restore", dependencies=[Depends(verify_token)])
|
||||
async def restore_stack(
|
||||
file: UploadFile = File(...),
|
||||
target_id: str | None = Form(None),
|
||||
overwrite: bool = Form(False),
|
||||
restore_volumes: bool = Form(True),
|
||||
) -> dict:
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz")
|
||||
try:
|
||||
while chunk := await file.read(1024 * 1024):
|
||||
tmp.write(chunk)
|
||||
tmp.close()
|
||||
target = compose_service.slugify(target_id) if target_id else None
|
||||
try:
|
||||
return backup_service.restore_backup(
|
||||
tmp.name, target_id=target, overwrite=overwrite, restore_volumes=restore_volumes,
|
||||
)
|
||||
except backup_service.BackupError as exc:
|
||||
code = 409 if "already exists" in str(exc) else 400
|
||||
raise HTTPException(status_code=code, detail=str(exc)) from exc
|
||||
finally:
|
||||
if os.path.exists(tmp.name):
|
||||
os.unlink(tmp.name)
|
||||
|
||||
|
||||
@app.get("/agent/health")
|
||||
def health() -> dict:
|
||||
return {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user