103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_campaign.backend.artifact_reconciliation import (
|
|
CampaignArtifactReconciliationError,
|
|
reconcile_campaign_artifacts,
|
|
)
|
|
from govoplan_campaign.backend.persistence.campaigns import _object_storage
|
|
from govoplan_campaign.backend.schemas import (
|
|
CampaignArtifactReconcileRequest,
|
|
CampaignArtifactReconcileResponse,
|
|
)
|
|
from govoplan_core.auth import ApiPrincipal, require_scope
|
|
from govoplan_core.audit.logging import audit_from_principal
|
|
from govoplan_core.core.object_storage import StorageBackendError
|
|
from govoplan_core.core.recovery import RecoveryGuaranteeError
|
|
from govoplan_core.core.recovery_runtime import (
|
|
RecoveryOperationBusy,
|
|
RecoveryOperationStateConflict,
|
|
)
|
|
from govoplan_core.db.session import get_database, get_session
|
|
from govoplan_core.server.runtime_agent import application_runtime_identity
|
|
|
|
|
|
router = APIRouter(prefix="/campaigns/operations", tags=["campaigns"])
|
|
|
|
|
|
@router.post(
|
|
"/artifacts/reconcile",
|
|
response_model=CampaignArtifactReconcileResponse,
|
|
)
|
|
def reconcile_artifacts(
|
|
payload: CampaignArtifactReconcileRequest,
|
|
request: Request,
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
|
|
) -> CampaignArtifactReconcileResponse:
|
|
"""Inventory or remove old, unreferenced Campaign-owned build objects."""
|
|
|
|
try:
|
|
result = reconcile_campaign_artifacts(
|
|
get_database().SessionLocal,
|
|
storage=_object_storage(),
|
|
identity=application_runtime_identity(request.app),
|
|
tenant_id=principal.tenant_id,
|
|
apply=payload.apply,
|
|
idempotency_key=payload.idempotency_key,
|
|
grace_period_hours=payload.grace_period_hours,
|
|
cursor=payload.cursor,
|
|
page_size=payload.page_size,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(exc),
|
|
) from exc
|
|
except (RecoveryOperationBusy, RecoveryOperationStateConflict) as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=str(exc),
|
|
) from exc
|
|
except (
|
|
CampaignArtifactReconciliationError,
|
|
RecoveryGuaranteeError,
|
|
StorageBackendError,
|
|
SQLAlchemyError,
|
|
OSError,
|
|
) as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail=(
|
|
"Campaign artifact reconciliation is temporarily unavailable "
|
|
f"({type(exc).__name__})."
|
|
),
|
|
) from exc
|
|
|
|
audit_from_principal(
|
|
session,
|
|
principal,
|
|
action=(
|
|
"campaign.artifact_orphans_reconciled"
|
|
if payload.apply
|
|
else "campaign.artifact_inventory_scanned"
|
|
),
|
|
object_type="campaign_artifact_namespace",
|
|
object_id=principal.tenant_id,
|
|
details={
|
|
"apply": payload.apply,
|
|
"status": result["status"],
|
|
"scanned_count": result["scanned_count"],
|
|
"candidate_count": result["candidate_count"],
|
|
"deleted_count": result["deleted_count"],
|
|
"failure_count": result["failure_count"],
|
|
"manifest_sha256": result["manifest_sha256"],
|
|
"recovery_operation_id": result["recovery_operation_id"],
|
|
},
|
|
commit=True,
|
|
)
|
|
return CampaignArtifactReconcileResponse.model_validate(result)
|