fester/backend/api/pipeline_control.py

128 lines
3.2 KiB
Python

"""
Pipeline control — retry / force-node / pause / resume / state.
Wraps an in-memory engine (set by main.py) and emits events on the bus
so the UI live stream picks them up.
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional, Any
from backend.api.api import bus
from backend.events.schema import EventType
router = APIRouter(prefix="/api/pipeline", tags=["pipeline"])
# In-memory engine reference (set by main.py)
_ENGINE: Optional[Any] = None
_PIPELINE_STATE: dict = {
"state": "idle", # idle | running | paused | complete | failed
"active_actions": [],
"completed": 0,
"failed": 0,
"queued": 0,
"build_id": None,
}
class ActionBody(BaseModel):
action: str
node: Optional[str] = None
class ForceBody(BaseModel):
action: str
node: str
def set_engine(engine):
global _ENGINE
_ENGINE = engine
def get_state_snapshot():
return dict(_PIPELINE_STATE)
# -----------------------------
# GET STATE
# -----------------------------
@router.get("/state")
def get_state():
return get_state_snapshot()
# -----------------------------
# RETRY ACTION
# -----------------------------
@router.post("/retry")
def retry_action(body: ActionBody):
bus.emit(
EventType.PIPELINE_UPDATE,
action=body.action,
node=body.node,
state="retrying",
reason="manual_retry",
)
# In a real engine this would re-queue the action.
# For now we just record the intent.
return {"status": "retry_queued", "action": body.action, "node": body.node}
# -----------------------------
# FORCE NODE
# -----------------------------
@router.post("/force-node")
def force_node(body: ForceBody):
bus.emit(
EventType.PIPELINE_UPDATE,
action=body.action,
node=body.node,
state="forced",
reason="manual_force",
)
return {"status": "forced", "action": body.action, "node": body.node}
# -----------------------------
# PAUSE
# -----------------------------
@router.post("/pause")
def pause():
_PIPELINE_STATE["state"] = "paused"
if _ENGINE and hasattr(_ENGINE, "pause"):
_ENGINE.pause()
bus.emit(EventType.PIPELINE_UPDATE, state="paused", reason="manual_pause")
return {"status": "paused"}
# -----------------------------
# RESUME
# -----------------------------
@router.post("/resume")
def resume():
_PIPELINE_STATE["state"] = "running"
if _ENGINE and hasattr(_ENGINE, "resume"):
_ENGINE.resume()
bus.emit(EventType.PIPELINE_UPDATE, state="running", reason="manual_resume")
return {"status": "running"}
# -----------------------------
# Legacy wrapper for api.py facade
# -----------------------------
def pipeline_control_endpoint(action, payload=None, bus=None):
"""Legacy dispatcher — `action` is one of: retry, force, pause, resume, state."""
if action == "retry":
return retry_action(ActionBody(**(payload or {})))
elif action == "force":
return force_node(ForceBody(**(payload or {})))
elif action == "pause":
return pause()
elif action == "resume":
return resume()
elif action == "state":
return get_state_snapshot()
return {"error": f"unknown action: {action}"}