Reconcile orphaned Campaign artifacts
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user