Files
Unraid-AppOps-Gateway-Public/backend/app/api/routes_enrollments.py
T

103 lines
3.4 KiB
Python

"""Operator API for the bounded Enrollment & Provisioning Plane v1."""
from __future__ import annotations
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
from pydantic import BaseModel, ConfigDict
from sqlmodel import Session
from ..core.config import settings
from ..core.security import require_operator_request
from ..db.session import get_session
from ..schemas.enrollment import EnrollmentPlanV1, EnrollmentTrustConfigV1
from ..services.enrollment_plane import EnrollmentError, EnrollmentPlane
from ..services.repopulse_adoption_journal import (
RepoPulseAdoptionJournalError,
get_repopulse_adoption_status,
)
def _require_operator(request: Request) -> str:
if not settings.operator_auth_required:
raise HTTPException(status_code=503, detail="Enrollment requires the configured operator authentication boundary.")
return require_operator_request(request)
router = APIRouter(
prefix="/api/enrollments",
tags=["enrollments"],
dependencies=[Depends(_require_operator)],
)
class EnrollmentRequestV1(BaseModel):
model_config = ConfigDict(extra="forbid")
plan: EnrollmentPlanV1
operation: Literal["preflight", "enroll"] = "preflight"
EnrollmentSession = Annotated[Session, Depends(get_session)]
EnrollmentIdempotencyKey = Annotated[str | None, Header(alias="Idempotency-Key")]
EnrollmentLimit = Annotated[int, Query(ge=1, le=250)]
def _idempotency(value: str | None) -> str:
if not value:
raise HTTPException(status_code=400, detail="Idempotency-Key is required for enrollment mutations.")
return value.strip()
@router.get("/capabilities")
def enrollment_capabilities(session: EnrollmentSession):
return EnrollmentPlane().capabilities(session)
@router.get("/trust")
def enrollment_trust_status(session: EnrollmentSession):
return EnrollmentPlane.trust_status(session)
@router.put("/trust")
def update_enrollment_trust(payload: EnrollmentTrustConfigV1, session: EnrollmentSession):
return EnrollmentPlane.set_trust_config(session, payload)
@router.post("")
def request_enrollment(
payload: EnrollmentRequestV1,
session: EnrollmentSession,
idempotency_key: EnrollmentIdempotencyKey = None,
):
plane = EnrollmentPlane()
try:
if payload.operation == "preflight":
return plane.preflight(session, payload.plan)
return plane.enroll(session, payload.plan, idempotency_key=_idempotency(idempotency_key))
except EnrollmentError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
@router.get("")
def list_enrollments(session: EnrollmentSession, limit: EnrollmentLimit = 100):
return {"items": EnrollmentPlane().list_enrollments(session, limit=limit)}
@router.get("/repopulse/adoption")
def repopulse_adoption_status(session: EnrollmentSession):
"""Read the sole fixed migration journal through the operator boundary."""
try:
return get_repopulse_adoption_status(session)
except RepoPulseAdoptionJournalError as exc:
raise HTTPException(status_code=503, detail=exc.code) from None
@router.get("/{enrollment_id}")
def get_enrollment(enrollment_id: str, session: EnrollmentSession):
try:
return EnrollmentPlane().get_enrollment(session, enrollment_id)
except EnrollmentError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc