fix(api): install ansible runtime for truth chain
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user