fix(api): install ansible runtime for truth chain
Some checks failed
CD Pipeline / tests (push) Failing after 1m39s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Successful in 11s

This commit is contained in:
Your Name
2026-05-31 12:20:41 +08:00
parent 04ac5085cd
commit da519423e1
5 changed files with 76 additions and 3 deletions

View File

@@ -71,8 +71,11 @@ COPY --chown=appuser:appuser apps/api/alert_rules.yaml ./alert_rules.yaml
# 2026-04-10 Claude Sonnet 4.6: drift_detector 需要 k8s/ YAML 做 Git state 比對
COPY --chown=appuser:appuser k8s/ ./k8s/
# 2026-05-24 Codex: truth-chain / Ansible readiness needs the repo-known
# playbook catalog in the API image. This does not install ansible-core or
# enable apply; it only lets operators see whether check-mode can be wired.
# playbook catalog in the API image.
# 2026-05-31 Codex: ansible-core is now installed through pyproject.toml so
# this catalog can graduate from visibility-only to check-mode runtime-ready
# once repair SSH material is mounted and readable. This still does not enable
# automatic apply; approval/execution code remains the gate.
COPY --chown=appuser:appuser infra/ansible/ ./infra/ansible/
# 2026-04-10 Claude Sonnet 4.6: RAG 知識庫索引來源 (ADR-067 Phase 33)
COPY --chown=appuser:appuser docs/ ./docs/

View File

@@ -46,6 +46,10 @@ dependencies = [
# 2026-04-16 ogt + Claude Sonnet 4.6: SSH MCP sensor 修復 — asyncssh 缺失導致 sensors_succeeded=0
# 根因: ssh_provider.py 中 import asyncssh 在 try/except 外,所有 15 個 SSH tool 直接 ImportError
"asyncssh>=2.14.0",
# 2026-05-31 Codex: AwoooP truth-chain Ansible runtime gate 需要
# production API image 內真的存在 ansible-playbook否則只能顯示
# candidate audit無法進入 check-mode executor readiness。
"ansible-core>=2.16.0,<2.18.0",
]
# [tool.uv.sources]

View File

@@ -58,3 +58,8 @@ pytest>=7.4.0
pytest-asyncio>=0.23.0
ruff>=0.1.0
sentry-sdk[fastapi]>=2.0.0
# AwoooP Ansible runtime readiness
# 2026-05-31 Codex: production API image must include ansible-playbook before
# truth-chain can honestly mark check-mode executor readiness as available.
ansible-core>=2.16.0,<2.18.0

View File

@@ -9,6 +9,7 @@ from __future__ import annotations
import asyncio
import json
import os
import shutil
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
@@ -716,7 +717,15 @@ def _ansible_playbook_roots(module_path: Path | None = None) -> list[Path]:
]
def _ansible_runtime_readiness() -> dict[str, Any]:
def _path_readable(path: Path) -> bool:
return path.exists() and path.is_file() and os.access(path, os.R_OK)
def _ansible_runtime_readiness(
*,
repair_ssh_key_path: Path = Path("/etc/repair-ssh/id_ed25519"),
repair_known_hosts_path: Path = Path("/etc/repair-known-hosts/known_hosts"),
) -> dict[str, Any]:
playbook_roots = _ansible_playbook_roots()
playbook_root = next((path for path in playbook_roots if path.exists()), None)
playbook_paths = (
@@ -726,6 +735,8 @@ def _ansible_runtime_readiness() -> dict[str, Any]:
)
inventory_path = playbook_root / "inventory" / "hosts.yml" if playbook_root is not None else None
binary_path = shutil.which("ansible-playbook")
repair_ssh_key_readable = _path_readable(repair_ssh_key_path)
repair_known_hosts_readable = _path_readable(repair_known_hosts_path)
blockers: list[str] = []
if not binary_path:
blockers.append("ansible_playbook_binary_missing")
@@ -735,6 +746,10 @@ def _ansible_runtime_readiness() -> dict[str, Any]:
blockers.append("ansible_inventory_missing")
if not playbook_paths:
blockers.append("ansible_playbooks_missing")
if not repair_ssh_key_readable:
blockers.append("ansible_repair_ssh_key_missing")
if not repair_known_hosts_readable:
blockers.append("ansible_repair_known_hosts_missing")
return {
"ansible_playbook_binary_present": bool(binary_path),
@@ -743,6 +758,12 @@ def _ansible_runtime_readiness() -> dict[str, Any]:
"playbook_root": str(playbook_root) if playbook_root is not None else None,
"inventory_present": bool(inventory_path and inventory_path.exists()),
"playbook_count": len(playbook_paths),
"repair_ssh_key_present": repair_ssh_key_path.exists(),
"repair_ssh_key_readable": repair_ssh_key_readable,
"repair_ssh_key_path": str(repair_ssh_key_path),
"repair_known_hosts_present": repair_known_hosts_path.exists(),
"repair_known_hosts_readable": repair_known_hosts_readable,
"repair_known_hosts_path": str(repair_known_hosts_path),
"can_run_check_mode": not blockers,
"blockers": blockers,
}

View File

@@ -706,6 +706,46 @@ def test_ansible_runtime_readiness_reports_check_mode_blockers() -> None:
assert readiness["playbook_count"] >= 1
assert isinstance(readiness["can_run_check_mode"], bool)
assert isinstance(readiness["blockers"], list)
assert "repair_ssh_key_readable" in readiness
assert "repair_known_hosts_readable" in readiness
def test_ansible_runtime_readiness_requires_repair_ssh_material(tmp_path: Path) -> None:
missing_key = tmp_path / "missing-id-ed25519"
missing_known_hosts = tmp_path / "missing-known-hosts"
readiness = _ansible_runtime_readiness(
repair_ssh_key_path=missing_key,
repair_known_hosts_path=missing_known_hosts,
)
assert readiness["repair_ssh_key_present"] is False
assert readiness["repair_ssh_key_readable"] is False
assert readiness["repair_known_hosts_present"] is False
assert readiness["repair_known_hosts_readable"] is False
assert "ansible_repair_ssh_key_missing" in readiness["blockers"]
assert "ansible_repair_known_hosts_missing" in readiness["blockers"]
def test_ansible_runtime_readiness_accepts_readable_repair_ssh_material(tmp_path: Path) -> None:
key_path = tmp_path / "id_ed25519"
known_hosts_path = tmp_path / "known_hosts"
key_path.write_text("test-key", encoding="utf-8")
known_hosts_path.write_text("192.168.0.110 ssh-ed25519 AAAATEST", encoding="utf-8")
key_path.chmod(0o400)
known_hosts_path.chmod(0o400)
readiness = _ansible_runtime_readiness(
repair_ssh_key_path=key_path,
repair_known_hosts_path=known_hosts_path,
)
assert readiness["repair_ssh_key_present"] is True
assert readiness["repair_ssh_key_readable"] is True
assert readiness["repair_known_hosts_present"] is True
assert readiness["repair_known_hosts_readable"] is True
assert "ansible_repair_ssh_key_missing" not in readiness["blockers"]
assert "ansible_repair_known_hosts_missing" not in readiness["blockers"]
def test_ansible_playbook_roots_supports_flat_container_module_path() -> None: