iac/docs/architecture/scan_contour.py

70 lines
3.3 KiB
Python

"""Строит матрицу «приложение -> используемый технологический сервис» для контура.
py scan_contour.py <имя-кластера> <выход.json>
Имя кластера — директория в clusters/, например любая из перечисленных
в корневом CLAUDE.md. Сканируются только *.yaml в apps/<app>/base и
apps/<app>/<кластер>; документация (*.md) игнорируется, иначе прозаические
упоминания переменных дают ложные срабатывания.
"""
import re, pathlib, json, sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
if len(sys.argv) != 3:
sys.exit(__doc__)
CLUSTER, OUT = sys.argv[1], pathlib.Path(sys.argv[2])
ROOT = pathlib.Path(__file__).resolve().parents[2]
kust = (ROOT / "clusters" / CLUSTER / "kustomization.yaml").read_text(encoding="utf-8")
apps = sorted(set(re.findall(r"\.\./\.\./apps/([a-z0-9-]+)/" + re.escape(CLUSTER), kust)))
if not apps:
sys.exit(f"в clusters/{CLUSTER}/kustomization.yaml не найдено ни одного приложения")
MARKERS = {
"postgres": [r"POSTGRES_[A-Z]", r"DB__HOST", r"postgresql\.postgresql\.svc",
r"secrets/data/apps/[a-z0-9-]+/postgres"],
"rabbitmq": [r"RABBITMQ", r"AMQP__", r"rabbitmq\.rabbitmq\.svc"],
"kafka": [r"KAFKA", r"kafka\.kafka\.svc", r"BOOTSTRAP_SERVERS"],
"camunda": [r"ZEEBE", r"CAMUNDA"],
"s3": [r"S3__", r"S3_ENDPOINT", r"S3_ACCESS", r"S3_BUCKET"],
"redis": [r"REDIS_HOST", r"REDIS__", r"redis\.[a-z-]+\.svc"],
"vault": [r"vault\.hashicorp\.com/agent-inject"],
"oidc": [r"ZITADEL", r"JWKS", r"OIDC", r"jwt-public"],
}
def scan(app):
blob = []
for sub in ("base", CLUSTER):
d = ROOT / "apps" / app / sub
if d.is_dir():
blob += [f.read_text(encoding="utf-8", errors="replace") for f in d.rglob("*.yaml")]
blob = "\n".join(blob)
return {svc: any(re.search(p, blob) for p in pats) for svc, pats in MARKERS.items()}
matrix = {a: scan(a) for a in apps}
istio_path = ROOT / "infrastructure" / "istio-config" / CLUSTER / "istio-config.yaml"
exposed_ns = []
if istio_path.is_file():
raw = istio_path.read_text(encoding="utf-8")
body = "\n".join(l for l in raw.splitlines() if not l.lstrip().startswith("#"))
exposed_ns = sorted({ns for _, ns in re.findall(r"service:\s*([a-z0-9-]+)\.([a-z0-9-]+)\.svc", body)})
hdr = ["app"] + list(MARKERS) + ["istio"]
print(f"приложений: {len(apps)}\n")
print(" | ".join(h.ljust(9) for h in hdr))
print("-" * 112)
for app, row in matrix.items():
cells = [app.ljust(17)] + ["+".ljust(9) if row[s] else "".ljust(9) for s in MARKERS]
cells.append("+".ljust(9) if app in exposed_ns else "".ljust(9))
print(" | ".join(cells))
print("\nпотребителей на сервис:")
for s in MARKERS:
print(f" {s:10s} {sum(1 for r in matrix.values() if r[s]):2d}")
print(f" {'istio':10s} {len([a for a in apps if a in exposed_ns]):2d}")
OUT.write_text(json.dumps({"apps": apps, "matrix": matrix, "exposed": exposed_ns},
ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\nзаписано: {OUT}")