257 lines
8.4 KiB
Python
257 lines
8.4 KiB
Python
"""Operator API for Deployment Control Plane V1.
|
|
|
|
All mutations accept a typed plan and an idempotency key. There is no endpoint
|
|
for command execution, Compose upload, arbitrary host paths or raw secrets.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated, Literal
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlmodel import Session
|
|
|
|
from ..core.security import require_operator_request
|
|
from ..db.session import get_session
|
|
from ..schemas.deployment import DeploymentPlanV1
|
|
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
|
from ..schemas.forge_bootstrap_apply import ForgeBootstrapApplyV1
|
|
from ..schemas.forge_secret_delivery import (
|
|
ForgeSecretDeliveryRequestV1,
|
|
ForgeSecretDeliveryResultV1,
|
|
)
|
|
from ..schemas.forge_runtime_readiness import ForgeRuntimeReadinessResultV1
|
|
from ..services.deployment_broker import DeploymentBrokerClient
|
|
from ..services.deployment_control_plane import (
|
|
DeploymentControlError,
|
|
DeploymentControlPlane,
|
|
)
|
|
|
|
|
|
def _require_deployment_operator(request: Request) -> str:
|
|
return require_operator_request(request)
|
|
|
|
|
|
router = APIRouter(
|
|
prefix="/api/deployments",
|
|
tags=["deployments"],
|
|
dependencies=[Depends(_require_deployment_operator)],
|
|
)
|
|
|
|
|
|
class DeploymentRequestV1(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
plan: DeploymentPlanV1
|
|
operation: Literal["preflight", "apply", "rollback"] = "preflight"
|
|
|
|
|
|
class WorkMeshPromotionPreflightRequest(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
revision: str = Field(pattern=r"^[0-9a-f]{40}$")
|
|
|
|
|
|
DeploymentSession = Annotated[Session, Depends(get_session)]
|
|
DeploymentIdempotencyKey = Annotated[str | None, Header(alias="Idempotency-Key")]
|
|
DeploymentLimit = Annotated[int, Query(ge=1, le=250)]
|
|
|
|
|
|
def _idempotency_header(value: str | None) -> str:
|
|
if not value:
|
|
raise HTTPException(status_code=400, detail="Idempotency-Key is required for deployment operations.")
|
|
return value.strip()
|
|
|
|
|
|
@router.post("")
|
|
def request_deployment(
|
|
payload: DeploymentRequestV1,
|
|
session: DeploymentSession,
|
|
idempotency_key: DeploymentIdempotencyKey = None,
|
|
):
|
|
try:
|
|
return DeploymentControlPlane().execute(
|
|
session,
|
|
plan=payload.plan,
|
|
idempotency_key=_idempotency_header(idempotency_key),
|
|
operation=payload.operation,
|
|
)
|
|
except DeploymentControlError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/workmesh/promotions/preflight")
|
|
def preflight_workmesh_promotion(
|
|
payload: WorkMeshPromotionPreflightRequest,
|
|
session: DeploymentSession,
|
|
idempotency_key: DeploymentIdempotencyKey = None,
|
|
):
|
|
from ..services.workmesh_promotion import (
|
|
WorkMeshPromotionError,
|
|
WorkMeshPromotionService,
|
|
)
|
|
|
|
try:
|
|
return WorkMeshPromotionService().preflight(
|
|
session,
|
|
revision=payload.revision,
|
|
idempotency_key=_idempotency_header(idempotency_key),
|
|
)
|
|
except WorkMeshPromotionError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/forge-bootstrap/preflight")
|
|
def preflight_forge_bootstrap(
|
|
payload: ForgeBootstrapPreflightV1,
|
|
session: DeploymentSession,
|
|
idempotency_key: DeploymentIdempotencyKey = None,
|
|
):
|
|
from ..services.forge_bootstrap_preflight import (
|
|
ForgeBootstrapPreflightError,
|
|
ForgeBootstrapPreflightService,
|
|
)
|
|
|
|
try:
|
|
return ForgeBootstrapPreflightService().preflight(
|
|
session,
|
|
payload,
|
|
idempotency_key=_idempotency_header(idempotency_key),
|
|
)
|
|
except ForgeBootstrapPreflightError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post(
|
|
"/forge-bootstrap/secrets",
|
|
response_model=ForgeSecretDeliveryResultV1,
|
|
)
|
|
def prepare_forge_bootstrap_secrets(
|
|
payload: ForgeSecretDeliveryRequestV1,
|
|
actor_identity: Annotated[str, Depends(_require_deployment_operator)],
|
|
session: DeploymentSession,
|
|
idempotency_key: DeploymentIdempotencyKey = None,
|
|
):
|
|
"""Deliver only the verified protected Forge inputs to the fixed controller."""
|
|
from ..services.forge_secret_delivery import prepare_forge_secret_delivery
|
|
|
|
try:
|
|
return prepare_forge_secret_delivery(
|
|
session,
|
|
profile=payload.profile,
|
|
idempotency_key=_idempotency_header(idempotency_key),
|
|
actor_identity=actor_identity,
|
|
)
|
|
except ValueError as exc:
|
|
# Service messages are fixed safe codes; never surface upstream text.
|
|
raise HTTPException(status_code=409, detail="forge_secret_delivery_failed") from exc
|
|
|
|
|
|
@router.post("/forge-bootstrap/first-install")
|
|
def apply_forge_first_install(
|
|
payload: ForgeBootstrapApplyV1,
|
|
session: DeploymentSession,
|
|
idempotency_key: DeploymentIdempotencyKey = None,
|
|
):
|
|
from ..services.forge_bootstrap_apply import (
|
|
ForgeBootstrapApplyError,
|
|
ForgeBootstrapApplyService,
|
|
)
|
|
|
|
try:
|
|
return ForgeBootstrapApplyService().apply(
|
|
session,
|
|
payload,
|
|
idempotency_key=_idempotency_header(idempotency_key),
|
|
)
|
|
except ForgeBootstrapApplyError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
|
|
|
|
@router.post("/forge-bootstrap/first-install/{job_id}/resume")
|
|
def resume_forge_first_install(job_id: str, session: DeploymentSession):
|
|
from ..services.forge_bootstrap_apply import ForgeBootstrapApplyService
|
|
|
|
result = ForgeBootstrapApplyService().resume(session, job_id)
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="Forge first-install request not found.")
|
|
return result
|
|
|
|
|
|
@router.get("/workmesh/promotions/{promotion_id}")
|
|
def get_workmesh_promotion(promotion_id: str, session: DeploymentSession):
|
|
from ..services.workmesh_promotion import WorkMeshPromotionService
|
|
|
|
result = WorkMeshPromotionService().refresh(session, promotion_id)
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="WorkMesh promotion not found.")
|
|
return result
|
|
|
|
|
|
@router.get("/workmesh/promotions")
|
|
def list_workmesh_promotions(
|
|
session: DeploymentSession,
|
|
limit: DeploymentLimit = 20,
|
|
):
|
|
from ..services.workmesh_promotion import WorkMeshPromotionService
|
|
|
|
return {"items": WorkMeshPromotionService.list_promotions(session, limit=limit)}
|
|
|
|
|
|
@router.get("/forge-bootstrap/first-install/{job_id}")
|
|
def get_forge_first_install(job_id: str, session: DeploymentSession):
|
|
from ..services.forge_bootstrap_apply import ForgeBootstrapApplyService
|
|
|
|
result = ForgeBootstrapApplyService().status(session, job_id)
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="Forge first-install request not found.")
|
|
return result
|
|
|
|
|
|
@router.get(
|
|
"/forge-bootstrap/first-install/{job_id}/readiness",
|
|
response_model=ForgeRuntimeReadinessResultV1,
|
|
)
|
|
def get_forge_first_install_readiness(
|
|
job_id: str,
|
|
request: Request,
|
|
session: DeploymentSession,
|
|
):
|
|
"""Return only a fresh, fixed controller observation for a known install."""
|
|
content_lengths = request.headers.getlist("content-length")
|
|
if (
|
|
request.query_params
|
|
or len(content_lengths) > 1
|
|
or (content_lengths and content_lengths[0] != "0")
|
|
or request.headers.getlist("transfer-encoding")
|
|
):
|
|
raise HTTPException(status_code=400, detail="invalid_readiness_request")
|
|
from ..services.forge_bootstrap_apply import ForgeBootstrapApplyService
|
|
|
|
return ForgeBootstrapApplyService().current_readiness(session, job_id)
|
|
|
|
|
|
@router.get("")
|
|
def list_deployments(session: DeploymentSession, limit: DeploymentLimit = 100):
|
|
return {"items": DeploymentControlPlane().list_jobs(session, limit=limit)}
|
|
|
|
|
|
@router.get("/delivery-status")
|
|
def get_delivery_status():
|
|
"""Expose the controller's bounded, secret-free runnerless delivery view."""
|
|
return DeploymentBrokerClient().delivery_status()
|
|
|
|
|
|
@router.get("/{job_id}")
|
|
def get_deployment(job_id: str, session: DeploymentSession):
|
|
if job_id.startswith("forge-install-"):
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Use the fixed Forge first-install status route for this job.",
|
|
)
|
|
try:
|
|
return DeploymentControlPlane().refresh(session, job_id)
|
|
except DeploymentControlError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|