fix(api): enqueue repair candidates for ansible check mode
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m40s
CD Pipeline / build-and-deploy (push) Successful in 6m32s
CD Pipeline / post-deploy-checks (push) Successful in 1m38s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-27 13:17:04 +08:00
parent 121e5b8861
commit d48fcfbde6
5 changed files with 231 additions and 5 deletions

View File

@@ -542,10 +542,15 @@ def build_ansible_decision_audit_payload(
or ""
)[:240],
}
controlled_queue = decision_path == "repair_candidate_controlled_queue"
output_payload = {
"not_used_reason": not_used_reason,
"decision_effect": "audit_only",
"next_required_step": "wire approval_execution to Ansible check-mode before apply",
"decision_effect": "check_mode_queue_ready" if controlled_queue else "audit_only",
"next_required_step": (
"awooop_ansible_check_mode_worker_claims_candidate"
if controlled_queue
else "wire approval_execution to Ansible check-mode before apply"
),
}
return {
"operation_type": "ansible_candidate_matched",

View File

@@ -31,6 +31,10 @@ from src.models.playbook import RiskLevel as PlaybookRiskLevel
from src.repositories.playbook_repository import get_playbook_repository
from src.services.action_parser import ActionKind, parse_kubectl_action
from src.services.auto_repair_service import AutoRepairService
from src.services.awooop_ansible_audit_service import (
build_ansible_decision_audit_payload,
record_ansible_decision_audit,
)
from src.services.awooop_deeplinks import work_item_url
from src.services.evidence_snapshot import EvidenceSnapshot
from src.services.incident_service import get_incident_service
@@ -71,11 +75,15 @@ class RepairCandidateService:
investigator: Any | None = None,
playbook_repository: Any | None = None,
auto_repair_service: AutoRepairService | None = None,
ansible_audit_builder: Any | None = None,
ansible_audit_recorder: Any | None = None,
) -> None:
self._incident_service = incident_service or get_incident_service()
self._investigator = investigator or get_pre_decision_investigator()
self._playbook_repository = playbook_repository or get_playbook_repository()
self._auto_repair = auto_repair_service or AutoRepairService()
self._ansible_audit_builder = ansible_audit_builder or build_ansible_decision_audit_payload
self._ansible_audit_recorder = ansible_audit_recorder or record_ansible_decision_audit
async def build_from_incident_id(
self,
@@ -139,7 +147,7 @@ class RepairCandidateService:
)
if not playbook_id:
blockers.append("playbook_not_matched")
return self._blocked_result(
return await self._blocked_result_with_controlled_handoff(
blockers=blockers,
metadata=metadata,
evidence=evidence,
@@ -148,12 +156,13 @@ class RepairCandidateService:
alertname=alertname,
target_resource=target_resource,
namespace=namespace,
severity=severity,
)
playbook = await self._playbook_repository.get_by_id(playbook_id)
if not playbook:
blockers.append("playbook_not_found")
return self._blocked_result(
return await self._blocked_result_with_controlled_handoff(
blockers=blockers,
metadata=metadata,
evidence=evidence,
@@ -162,6 +171,7 @@ class RepairCandidateService:
alertname=alertname,
target_resource=target_resource,
namespace=namespace,
severity=severity,
)
metadata["playbook_trust"] = {
@@ -181,7 +191,7 @@ class RepairCandidateService:
step, step_blockers = self._select_executable_step(incident, playbook)
blockers.extend(step_blockers)
if blockers or step is None:
return self._blocked_result(
return await self._blocked_result_with_controlled_handoff(
blockers=blockers,
metadata=metadata,
evidence=evidence,
@@ -191,6 +201,7 @@ class RepairCandidateService:
alertname=alertname,
target_resource=target_resource,
namespace=namespace,
severity=severity,
)
metadata["repair_candidate"] = {
@@ -255,6 +266,116 @@ class RepairCandidateService:
metadata=metadata,
)
async def _blocked_result_with_controlled_handoff(
self,
*,
blockers: list[str],
metadata: dict[str, Any],
fallback_action: str,
evidence: EvidenceSnapshot | None = None,
playbook: Playbook | None = None,
incident: Incident | None = None,
alertname: str = "",
target_resource: str = "",
namespace: str = "",
severity: str | None = None,
) -> RepairCandidateResult:
result = self._blocked_result(
blockers=blockers,
metadata=metadata,
evidence=evidence,
playbook=playbook,
fallback_action=fallback_action,
incident=incident,
alertname=alertname,
target_resource=target_resource,
namespace=namespace,
)
await self._maybe_record_controlled_executor_handoff(
result=result,
incident=incident,
severity=severity,
)
return result
async def _maybe_record_controlled_executor_handoff(
self,
*,
result: RepairCandidateResult,
incident: Incident | None,
severity: str | None = None,
) -> None:
if incident is None or not result.metadata.get("repair_candidate_draft_ready"):
return
draft_package = result.metadata.get("repair_candidate_draft_package")
if not isinstance(draft_package, dict):
return
promotion_contract = draft_package.get("candidate_promotion_contract")
if not isinstance(promotion_contract, dict):
return
proposal_data = {
"source": "repair_candidate_controlled_queue",
"risk_level": severity or getattr(getattr(incident, "severity", None), "value", ""),
"action": promotion_contract.get("repair_command_template") or "",
"matched_playbook_id": draft_package.get("matched_playbook_id"),
"repair_candidate_status": result.metadata.get("repair_candidate_status"),
"route_id": promotion_contract.get("route_id"),
"controlled_playbook_queue": True,
}
payload = self._ansible_audit_builder(
incident=incident,
proposal_data=proposal_data,
decision_path="repair_candidate_controlled_queue",
not_used_reason=(
"repair candidate controlled queue ready; "
"Ansible check-mode worker should claim candidate"
),
)
if payload is None:
handoff = {
"schema_version": "repair_candidate_controlled_executor_handoff_v1",
"status": "no_ansible_catalog_candidate",
"operation_type": "ansible_candidate_matched",
"candidate_count": 0,
"check_mode_worker": "awooop_ansible_check_mode_worker",
"check_mode_queued": False,
"runtime_execution_authorized": False,
}
else:
written = await self._ansible_audit_recorder(
incident=incident,
proposal_data=proposal_data,
decision_path="repair_candidate_controlled_queue",
not_used_reason=(
"repair candidate controlled queue ready; "
"Ansible check-mode worker should claim candidate"
),
)
candidates = payload.get("input", {}).get("executor_candidates")
candidate_count = len(candidates) if isinstance(candidates, list) else 0
handoff = {
"schema_version": "repair_candidate_controlled_executor_handoff_v1",
"status": (
"ansible_candidate_matched_queued"
if written
else "ansible_candidate_matched_existing_or_write_skipped"
),
"operation_type": payload.get("operation_type"),
"decision_effect": payload.get("output", {}).get("decision_effect"),
"next_required_step": payload.get("output", {}).get("next_required_step"),
"candidate_count": candidate_count,
"check_mode_worker": "awooop_ansible_check_mode_worker",
"check_mode_queued": True,
"runtime_execution_authorized": False,
}
result.metadata["controlled_executor_handoff"] = handoff
draft_package["controlled_executor_handoff"] = handoff
work_item = draft_package.get("awooop_work_item")
if isinstance(work_item, dict):
work_item["controlled_executor_handoff"] = handoff
async def _collect_evidence(self, incident: Incident) -> EvidenceSnapshot | None:
try:
return await self._investigator.investigate(incident)