feat: add approved stack lifecycle endpoints
This commit is contained in:
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
|
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [qyburn] %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [qyburn] %(message)s")
|
||||||
@@ -709,3 +709,83 @@ async def receive_handoff(job_id: str, request: Request):
|
|||||||
logger.info(f"[{job_id[:8]}] handoff received — pending review")
|
logger.info(f"[{job_id[:8]}] handoff received — pending review")
|
||||||
_write_last_run()
|
_write_last_run()
|
||||||
return {"status": "pending_review", "job_id": job_id}
|
return {"status": "pending_review", "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Jon Snow approval gate — rebuild/restart endpoints (pure subprocess, no LLM)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.post("/rebuild/{stack_name}")
|
||||||
|
async def rebuild_stack(stack_name: str):
|
||||||
|
"""Rebuild a Docker Compose stack (compose up -d --build). Called by Jon Snow approval gate.
|
||||||
|
No LLM involved — pure subprocess execution."""
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', stack_name):
|
||||||
|
return JSONResponse({"ok": False, "error": "invalid stack name"}, status_code=400)
|
||||||
|
|
||||||
|
stack_path = STACKS_DIR / stack_name
|
||||||
|
if not stack_path.exists():
|
||||||
|
return JSONResponse({"ok": False, "error": f"stack not found: {stack_name}"}, status_code=404)
|
||||||
|
|
||||||
|
logger.info(f"[rebuild] {stack_name} — starting compose up -d --build")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "compose", "up", "-d", "--build"],
|
||||||
|
cwd=str(stack_path),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
ok = result.returncode == 0
|
||||||
|
logger.info(f"[rebuild] {stack_name} — {'ok' if ok else 'failed'} (rc={result.returncode})")
|
||||||
|
return JSONResponse({
|
||||||
|
"ok": ok,
|
||||||
|
"stack": stack_name,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": result.stdout[-2000:] if result.stdout else "",
|
||||||
|
"stderr": result.stderr[-2000:] if result.stderr else "",
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error(f"[rebuild] {stack_name} — timed out after 300s")
|
||||||
|
return JSONResponse({"ok": False, "error": "rebuild timed out (300s)"}, status_code=408)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[rebuild] {stack_name} — error: {e}")
|
||||||
|
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/restart/{stack_name}")
|
||||||
|
async def restart_stack(stack_name: str):
|
||||||
|
"""Restart a Docker Compose stack without rebuilding (compose restart). Called by Jon Snow approval gate.
|
||||||
|
No LLM involved — pure subprocess execution."""
|
||||||
|
import re
|
||||||
|
if not re.match(r'^[a-zA-Z0-9_-]+$', stack_name):
|
||||||
|
return JSONResponse({"ok": False, "error": "invalid stack name"}, status_code=400)
|
||||||
|
|
||||||
|
stack_path = STACKS_DIR / stack_name
|
||||||
|
if not stack_path.exists():
|
||||||
|
return JSONResponse({"ok": False, "error": f"stack not found: {stack_name}"}, status_code=404)
|
||||||
|
|
||||||
|
logger.info(f"[restart] {stack_name} — starting compose restart")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "compose", "restart"],
|
||||||
|
cwd=str(stack_path),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
ok = result.returncode == 0
|
||||||
|
logger.info(f"[restart] {stack_name} — {'ok' if ok else 'failed'} (rc={result.returncode})")
|
||||||
|
return JSONResponse({
|
||||||
|
"ok": ok,
|
||||||
|
"stack": stack_name,
|
||||||
|
"returncode": result.returncode,
|
||||||
|
"stdout": result.stdout[-2000:] if result.stdout else "",
|
||||||
|
"stderr": result.stderr[-2000:] if result.stderr else "",
|
||||||
|
})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error(f"[restart] {stack_name} — timed out after 120s")
|
||||||
|
return JSONResponse({"ok": False, "error": "restart timed out (120s)"}, status_code=408)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[restart] {stack_name} — error: {e}")
|
||||||
|
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
||||||
|
|||||||
Reference in New Issue
Block a user