100 lines
3.2 KiB
Python
100 lines
3.2 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.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:
|
|
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
|