Files
acdl/core/metrics/infracost_adapter.py
T
Jon Chery f8616b806e feat(P1): event emitters — CloudEvents envelope, Decision Ledger, Infracost adapter, attestation/confidence/policy event emission
P1 (Wave 1, feat) — REQ-187, REQ-188, REQ-205 (emitter), REQ-206 (emitter)

New components:
- core/metrics/event_envelope.py — CloudEvents 1.0 envelope + platform.* conventions
- core/metrics/run_manifest.py — per-run manifest writer (nova.run.started/completed/failed)
- core/metrics/decision_ledger.py — SQLite append-only hash-chain (ai.decision.made + attestation.recorded)
- core/metrics/infracost_adapter.py — Infracost post-processor (degraded mode when CLI absent, A6)
- schemas/metrics_event.schema.json — CloudEvents envelope schema
- schemas/metrics_run_manifest.schema.json — per-run manifest schema
- metrics/README.md — backup/restore doc (REQ-201)
- tests/test_metrics_emitters.py — 16 tests (all pass)

Modified components:
- core/confidence_signal.py — emits nova.confidence.computed + nova.ai.decision.made (D-122)
- core/hitl_gates.py — emits nova.attestation.recorded on qa/prod/dr gates (D-132)
- adapters/terraform/policy/checkov_adapter.py — emits nova.policy.evaluated
- pyproject.toml — addopts gains --junitxml + --json-report + --cov (REQ-206)
- .gitignore — metrics runtime artifacts ignored

D-120: Nova-native (JSONL + SQLite, no Kafka/OTel)
D-121: Decision Ledger = outbox_writer extension → SQLite hash-chain
D-122: AI decision = confidence_signal + HITL gate (not LLM)
D-128: metrics/ at repo root
D-132: Attestation instrumentation

---ci---
project: acdl
phase: 1
milestone: v1.17
status: execute
---/ci---
2026-08-04 19:58:54 +00:00

73 lines
2.8 KiB
Python

"""Nova Infracost Post-Processor (REQ-187, D-120).
Runs Infracost on `terraform show -json plan.tfplan` (offline, reads plan
JSON, no live AWS). Emits nova.cost.estimated{delta_usd} events. Degrades
gracefully (omits the event, logs a warning) when Infracost CLI is absent
(assumption A6).
run_platform.sh invokes it after the plan stage.
"""
import json
import os
import shutil
import subprocess
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.metrics.event_envelope import emit
def _is_infracost_available():
"""Check if the Infracost CLI is on PATH."""
return shutil.which("infracost") is not None
def estimate(plan_json_path, run_id, contract_id, environment):
"""Run Infracost on a terraform plan JSON. Returns the cost estimate dict.
Args:
plan_json_path: path to `terraform show -json plan.tfplan` output
run_id: the run identifier
contract_id: the contract UUID
environment: dev|qa|prod|dr
Returns:
{"delta_usd": float, "total_monthly_usd": float, "available": bool}
or {"available": False} if Infracost is not installed.
"""
if not _is_infracost_available():
sys.stderr.write("[infracost] CLI not found — cost.estimated event omitted (A6 degraded mode)\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
if not os.path.isfile(plan_json_path):
sys.stderr.write(f"[infracost] plan JSON not found: {plan_json_path}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
try:
result = subprocess.run(
["infracost", "breakdown", "--path", plan_json_path, "--format", "json"],
capture_output=True, text=True, timeout=30,
)
if result.returncode != 0:
sys.stderr.write(f"[infracost] CLI failed: {result.stderr[:200]}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
breakdown = json.loads(result.stdout)
delta = float(breakdown.get("diffTotalMonthlyCost", 0.0))
total = float(breakdown.get("totalMonthlyCost", 0.0))
estimate_data = {"available": True, "delta_usd": delta, "total_monthly_usd": total}
emit("nova.cost.estimated", run_id, environment, estimate_data, contract_id=contract_id)
return estimate_data
except Exception as exc:
sys.stderr.write(f"[infracost] error: {exc}\n")
return {"available": False, "delta_usd": 0.0, "total_monthly_usd": 0.0}
if __name__ == "__main__":
if len(sys.argv) < 5:
print("usage: infracost_adapter.py <plan_json_path> <run_id> <contract_id> <environment>", file=sys.stderr)
sys.exit(2)
est = estimate(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
print(json.dumps(est, indent=2))