Merge remote-tracking branch 'gitea-ssh/main' into codex/p0-b-incident-closure-20260722

This commit is contained in:
Your Name
2026-07-22 18:07:05 +08:00
6 changed files with 131 additions and 6 deletions

View File

@@ -70,6 +70,7 @@ _WAZUH_INCOMPLETE_RUNTIME_RETRY_REASONS = frozenset(
"controlled_apply_failed",
"post_apply_learning_incomplete",
"recipient_visible_transition_missing",
"scheduled_candidate_recurrence_expired_before_claim",
}
)
_WAZUH_INGRESS_CATALOG_ID = "ansible:wazuh-alertmanager-integration"
@@ -1219,6 +1220,8 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once(
telegram.send_status AS latest_telegram_send_status,
telegram.provider_message_id
AS latest_telegram_provider_message_id,
execution_skip.input ->> 'not_used_reason'
AS latest_execution_skip_reason,
EXISTS (
SELECT 1
FROM incident_evidence evidence
@@ -1290,6 +1293,14 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once(
ORDER BY packet.created_at DESC
LIMIT 1
) capability_packet ON TRUE
LEFT JOIN LATERAL (
SELECT skipped.input
FROM automation_operation_log skipped
WHERE skipped.parent_op_id = candidate.op_id
AND skipped.operation_type = 'ansible_execution_skipped'
ORDER BY skipped.created_at DESC, skipped.op_id DESC
LIMIT 1
) execution_skip ON TRUE
LEFT JOIN LATERAL (
SELECT outbound.send_status,
outbound.provider_message_id
@@ -1681,6 +1692,10 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once(
retry_attempt_ref = f"{deploy_ref}-learning"
elif retry_reason == "recipient_visible_transition_missing":
retry_attempt_ref = f"{deploy_ref}-receipt"
elif retry_reason == (
"scheduled_candidate_recurrence_expired_before_claim"
):
retry_attempt_ref = f"{deploy_ref}-recurrence"
elif latest_check_status == "success":
retry_attempt_ref = f"{deploy_ref}-context"
retry_reason = "predecision_context"
@@ -2001,6 +2016,11 @@ def _wazuh_posture_retry_reason(
check_status = str(existing_row.get("latest_check_status") or "")
apply_status = str(existing_row.get("latest_apply_status") or "")
learning_status = str(existing_row.get("latest_learning_status") or "")
execution_skip_reason = str(
existing_row.get("latest_execution_skip_reason") or ""
)
if execution_skip_reason == "scheduled_candidate_identity_chain_not_verified":
return "scheduled_candidate_recurrence_expired_before_claim"
if check_status == "failed":
return "check_mode_failed"
if apply_status == "failed":

View File

@@ -2764,6 +2764,111 @@ async def test_wazuh_posture_scheduler_queues_one_recipient_transition_per_deplo
duplicate_collector.assert_not_awaited()
@pytest.mark.asyncio
async def test_wazuh_posture_scheduler_retries_expired_preclaim_recurrence_once_per_deploy(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.jobs import awooop_ansible_candidate_backfill_job as job
existing_row = {
"op_id": "00000000-0000-0000-0000-000000000327",
"candidate_status": "dry_run",
"automation_run_id": "00000000-0000-0000-0000-000000000327",
"source_receipt_ref": "scheduled-wazuh-manager-posture:2026071100",
"latest_check_status": None,
"latest_apply_status": None,
"latest_learning_status": None,
"latest_execution_skip_reason": (
"scheduled_candidate_identity_chain_not_verified"
),
"predecision_evidence_ready": True,
}
class _Mappings:
def first(self):
return existing_row
class _Result:
def mappings(self):
return _Mappings()
observed_sql: list[str] = []
class _Db:
async def execute(self, statement, _params):
observed_sql.append(str(statement))
return _Result()
@asynccontextmanager
async def fake_db_context(project_id: str):
assert project_id == "awoooi"
yield _Db()
recorder = AsyncMock(return_value=True)
monkeypatch.setattr(job, "get_db_context", fake_db_context)
monkeypatch.setattr(
job.settings,
"ENABLE_IWOOOS_WAZUH_MANAGER_POSTURE_EXECUTOR",
True,
)
monkeypatch.setattr(
job.settings,
"IWOOOS_WAZUH_MANAGER_POSTURE_FRESHNESS_HOURS",
6,
)
monkeypatch.setattr(
job,
"now_taipei",
MagicMock(return_value=datetime.fromisoformat("2026-07-11T05:59:59+08:00")),
)
monkeypatch.setenv(
"AWOOOI_BUILD_COMMIT_SHA",
"abcdef1234567890abcdef1234567890abcdef12",
)
result = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-6")),
evidence_verifier=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor_retry"
assert result["retry_reason"] == (
"scheduled_candidate_recurrence_expired_before_claim"
)
assert result["retry_of_incomplete_runtime_closure"] is True
assert "ansible_execution_skipped" in observed_sql[0]
assert "latest_execution_skip_reason" in observed_sql[0]
recorded = recorder.await_args.kwargs
assert recorded["incident"]["source_receipt_ref"] == (
"scheduled-wazuh-manager-posture:2026071100-ABCDEF123456-RECURRENCE"
)
existing_row["automation_run_id"] = result["automation_run_id"]
existing_row["source_receipt_ref"] = recorded["incident"]["source_receipt_ref"]
duplicate_collector = AsyncMock()
duplicate = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=duplicate_collector,
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert duplicate["status"] == (
"incomplete_runtime_retry_already_attempted_for_deploy"
)
assert duplicate["queued"] == 0
assert duplicate["retry_reason"] == (
"scheduled_candidate_recurrence_expired_before_claim"
)
recorder.assert_awaited_once()
duplicate_collector.assert_not_awaited()
@pytest.mark.asyncio
async def test_wazuh_posture_scheduler_does_not_duplicate_active_candidate(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -157,12 +157,12 @@ spec:
- name: AWOOOI_BUILD_COMMIT_SHA
# 2026-06-29 Codex: CD rewrites this to the deployed image tag so
# production deploy readback does not rely on a stale static snapshot.
value: "a857fde0df4b889951397a76f0c2cb9e8651b035"
value: "34d6f88f0c21db0d2739deec096865986d2aa075"
- name: AWOOOI_DESIRED_API_IMAGE_TAG
# 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA.
# Production readback compares runtime image truth against this
# GitOps desired tag instead of doing a slow Gitea raw fetch.
value: "a857fde0df4b889951397a76f0c2cb9e8651b035"
value: "34d6f88f0c21db0d2739deec096865986d2aa075"
- name: DATABASE_POOL_SIZE
# 2026-07-01 Codex: production role `awoooi` currently has a low
# connection limit. Keep API pool conservative until DB role

View File

@@ -66,7 +66,7 @@ spec:
- name: DATABASE_NULL_POOL
value: "true"
- name: AWOOOI_BUILD_COMMIT_SHA
value: "a857fde0df4b889951397a76f0c2cb9e8651b035"
value: "34d6f88f0c21db0d2739deec096865986d2aa075"
- name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER
value: "false"
- name: ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER

View File

@@ -178,7 +178,7 @@ spec:
- name: DATABASE_BOOTSTRAP_DDL_ENABLED
value: "false"
- name: AWOOOI_BUILD_COMMIT_SHA
value: "a857fde0df4b889951397a76f0c2cb9e8651b035"
value: "34d6f88f0c21db0d2739deec096865986d2aa075"
- name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER
value: "true"
- name: ENABLE_SECURITY_CONTROL_PLANE_MAINTENANCE_WORKER

View File

@@ -40,9 +40,9 @@ resources:
# ⚠️ 重要: name 必須與 deployment YAML 中的 image 完全匹配 (含 tag)
# newName + newTag 由 CI 透過 kustomize edit set image 注入
images:
- digest: sha256:c4d9c6e987979642fcbb049df60dfa84ba703423267c7c5caf7fc80212fbaefe
- digest: sha256:28a2e757088df35be1a4aae4347741ed2ba07e17b0cb7d36449f292c0f6e3db1
name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/api
- digest: sha256:69d8d6d98c13e77e3026350facc5fc803711281fdae05dd46caecdd863f20a25
- digest: sha256:3572331aed1836436dfc1dafd3c06b873b199b949776d00743754f9f84782010
name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/web