chore: release v1.0.0 — каркас FastAPI, ingress Grafana, интеграции, документация
Made-with: Cursor
This commit is contained in:
1
onguard24/integrations/__init__.py
Normal file
1
onguard24/integrations/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Внешние интеграции (Grafana, Vault, …)."""
|
||||
103
onguard24/integrations/forgejo_api.py
Normal file
103
onguard24/integrations/forgejo_api.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Forgejo / Gitea HTTP API: Authorization: token <secret>."""
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _auth(token: str) -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
||||
async def probe(base_url: str, token: str) -> dict:
|
||||
"""
|
||||
Проверка токена. Сначала GET /api/v1/user (нужен scope read:user).
|
||||
При 403 из‑за scope — fallback: GET /api/v1/admin/config (часто доступен с write:admin).
|
||||
"""
|
||||
if not base_url.strip() or not token.strip():
|
||||
return {"status": "error", "detail": "forgejo url or token empty"}
|
||||
|
||||
base = base_url.rstrip("/")
|
||||
h = _auth(token)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0, verify=True, follow_redirects=True) as client:
|
||||
ru = await client.get(f"{base}/api/v1/user", headers=h)
|
||||
if ru.status_code == 200:
|
||||
try:
|
||||
u = ru.json()
|
||||
except Exception:
|
||||
u = {}
|
||||
login = u.get("login")
|
||||
out: dict = {
|
||||
"status": "ok",
|
||||
"url": base,
|
||||
"api": "authenticated",
|
||||
"scope": "read:user",
|
||||
}
|
||||
if login:
|
||||
out["login"] = login
|
||||
return out
|
||||
|
||||
if ru.status_code == 403:
|
||||
for path, name in (
|
||||
("/api/v1/admin/config", "admin_config"),
|
||||
("/api/v1/notifications?limit=1", "notifications"),
|
||||
):
|
||||
rx = await client.get(f"{base}{path}", headers=h)
|
||||
if rx.status_code == 200:
|
||||
return {
|
||||
"status": "ok",
|
||||
"url": base,
|
||||
"api": "authenticated",
|
||||
"scope_note": (
|
||||
"в токене нет scope read:user — в Forgejo включи read:user у PAT "
|
||||
"или создай новый токен с read:user, чтобы отображался login"
|
||||
),
|
||||
"fallback": name,
|
||||
}
|
||||
|
||||
body = (ru.text or "")[:500]
|
||||
return {
|
||||
"status": "error",
|
||||
"url": base,
|
||||
"detail": f"http {ru.status_code}: {body}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e), "url": base_url.rstrip("/")}
|
||||
|
||||
|
||||
async def ping(base_url: str, token: str) -> tuple[bool, str | None]:
|
||||
"""Обёртка для совместимости: True если status ok."""
|
||||
r = await probe(base_url, token)
|
||||
if r.get("status") == "ok":
|
||||
return True, None
|
||||
return False, r.get("detail", "unknown")
|
||||
|
||||
|
||||
async def get_user(base_url: str, token: str) -> tuple[dict | None, str | None]:
|
||||
base = base_url.rstrip("/")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0, verify=True, follow_redirects=True) as client:
|
||||
r = await client.get(f"{base}/api/v1/user", headers=_auth(token))
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
if r.status_code != 200:
|
||||
return None, f"http {r.status_code}"
|
||||
try:
|
||||
return r.json(), None
|
||||
except Exception:
|
||||
return None, "invalid json"
|
||||
|
||||
|
||||
async def health_public(base_url: str) -> tuple[bool, str | None]:
|
||||
base = base_url.rstrip("/")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, verify=True) as client:
|
||||
r = await client.get(f"{base}/api/v1/version")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
if r.status_code == 200:
|
||||
return True, None
|
||||
return False, f"http {r.status_code}"
|
||||
58
onguard24/integrations/grafana_api.py
Normal file
58
onguard24/integrations/grafana_api.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""HTTP API Grafana: service account token (Bearer), не пароль пользователя."""
|
||||
|
||||
import httpx
|
||||
|
||||
# Минимальный набор для проверки и будущих вызовов (алерты, папки и т.д.).
|
||||
|
||||
|
||||
async def ping(base_url: str, token: str) -> tuple[bool, str | None]:
|
||||
if not base_url.strip() or not token.strip():
|
||||
return False, "grafana url or token empty"
|
||||
base = base_url.rstrip("/")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0, verify=True, follow_redirects=True) as client:
|
||||
r = await client.get(
|
||||
f"{base}/api/org",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
if r.status_code == 200:
|
||||
return True, None
|
||||
body = (r.text or "")[:300]
|
||||
return False, f"http {r.status_code}: {body}"
|
||||
|
||||
|
||||
async def get_signed_in_user(base_url: str, token: str) -> tuple[dict | None, str | None]:
|
||||
"""GET /api/user — удобно убедиться, что токен от service account."""
|
||||
base = base_url.rstrip("/")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0, verify=True, follow_redirects=True) as client:
|
||||
r = await client.get(
|
||||
f"{base}/api/user",
|
||||
headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
|
||||
)
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
if r.status_code != 200:
|
||||
return None, f"http {r.status_code}"
|
||||
try:
|
||||
return r.json(), None
|
||||
except Exception:
|
||||
return None, "invalid json"
|
||||
|
||||
|
||||
async def health_live(base_url: str) -> tuple[bool, str | None]:
|
||||
"""GET /api/health — без авторизации, проверка что инстанс отвечает."""
|
||||
base = base_url.rstrip("/")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0, verify=True) as client:
|
||||
r = await client.get(f"{base}/api/health")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
if r.status_code == 200:
|
||||
return True, None
|
||||
return False, f"http {r.status_code}"
|
||||
Reference in New Issue
Block a user