Health-Check: DNS-over-HTTPS-Rückfall bei nicht auflösbarem Hostnamen, DNS-Problem des Admin-Servers nur Warnung (außer strikte Rollouts); COST_INGEST_KEY optional (Schema-getrieben)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from sqlalchemy import select
|
||||
|
||||
from app.db import session
|
||||
from app.models import Deployment, Secret, Setting, Tenant
|
||||
from app.services import contract, crypto, guard, onyva_login, store
|
||||
from app.services import contract, crypto, guard, netcheck, onyva_login, store
|
||||
from app.services.mittwald import DeployError, Mittwald, mask_env
|
||||
|
||||
PORT = "8276"
|
||||
@@ -162,9 +162,10 @@ def _hex_tag(tag: str) -> bool:
|
||||
class HealthTimeout(DeployError):
|
||||
"""Health-Check nicht grün geworden. answered=False: es kam nie eine gültige Antwort (Zertifikat/DNS/Container noch nicht bereit)."""
|
||||
|
||||
def __init__(self, msg: str, answered: bool):
|
||||
def __init__(self, msg: str, answered: bool, dns: bool = False):
|
||||
super().__init__(msg)
|
||||
self.answered = answered
|
||||
self.dns = dns # Hostname war vom Admin-Server aus nicht auflösbar (Resolver-Cache/DNS), nicht Fehler des Containers
|
||||
|
||||
|
||||
def verify_health(host: str, expect_sha: str | None, expect_bundle: int | None, log, *, get=None, tries: int = 18, delay: float = 5.0,
|
||||
@@ -172,12 +173,8 @@ def verify_health(host: str, expect_sha: str | None, expect_bundle: int | None,
|
||||
"""Prüft nach dem Deploy, ob wirklich läuft, was wir ausgerollt haben (Vertrag: /health.core / .bundle).
|
||||
Wirft DeployError, wenn der Container nach `tries` Versuchen nicht gesund ist oder die falsche Version meldet."""
|
||||
if get is None:
|
||||
import httpx
|
||||
|
||||
def get(url):
|
||||
r = httpx.get(url, timeout=8, follow_redirects=True)
|
||||
return r.status_code, (r.json() if r.headers.get("content-type", "").startswith("application/json") else {})
|
||||
last, answered = "keine Antwort", False
|
||||
get = netcheck.get_json
|
||||
last, answered, dns = "keine Antwort", False, False
|
||||
for i in range(tries):
|
||||
try:
|
||||
status, body = get(f"https://{host}/health")
|
||||
@@ -197,18 +194,21 @@ def verify_health(host: str, expect_sha: str | None, expect_bundle: int | None,
|
||||
problems.append(f"Bundle {b.get('source')} v{b.get('version')}{' (veraltet)' if b.get('stale') else ''} statt admin v{expect_bundle}"
|
||||
+ (f" – {b.get('error')}" if b.get("error") else ""))
|
||||
if not problems:
|
||||
if body.get("_dns_fallback"):
|
||||
log("(Hinweis: Hostname vom Admin-Server nicht auflösbar – Prüfung über DNS-over-HTTPS direkt zur IP)")
|
||||
log(f"Health-Check https://{host}/health ✔" + (f" · Core {core['version'][:10]}" if core else "")
|
||||
+ (f" · Bundle v{body['bundle']['version']}" if body.get("bundle", {}).get("source") == "admin" else ""))
|
||||
return
|
||||
last = "; ".join(problems)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last = f"{type(exc).__name__}: {exc}"
|
||||
dns = netcheck.is_dns_failure_text(last)
|
||||
if i < tries - 1:
|
||||
sleep(delay)
|
||||
raise HealthTimeout(f"Health-Check fehlgeschlagen nach {tries} Versuchen: {last}", answered)
|
||||
raise HealthTimeout(f"Health-Check fehlgeschlagen nach {tries} Versuchen: {last}", answered, dns and not answered)
|
||||
|
||||
|
||||
def execute(dep_id: int, tenant_id: str, kind: str, http=None, oidc_client=None, core_tag: str | None = None, health=None) -> bool:
|
||||
def execute(dep_id: int, tenant_id: str, kind: str, http=None, oidc_client=None, core_tag: str | None = None, health=None, strict: bool = False) -> bool:
|
||||
"""Führt einen Deployment-Datensatz aus. kind: dry-run | deploy | offboard-dry | offboard.
|
||||
core_tag überschreibt den Core-Tag (Rollout/Rollback); `health(host, expect_sha, expect_bundle, log)` ist in Tests ersetzbar."""
|
||||
log = lambda m: _append(dep_id, m) # noqa: E731
|
||||
@@ -276,11 +276,16 @@ def execute(dep_id: int, tenant_id: str, kind: str, http=None, oidc_client=None,
|
||||
args = (host, tag, ver if source == "admin" else None, log)
|
||||
health(*args) if health else verify_health(*args, **kw)
|
||||
except HealthTimeout as exc:
|
||||
if fresh and not exc.answered: # nur „noch nicht erreichbar“ bei neuem Ingress: Deploy ist durch, Prüfung später wiederholen
|
||||
# Warnung statt Fehler: neuer Ingress ohne Antwort (Zertifikat/DNS) – oder (außerhalb strikter Rollouts) ein Namensauflösungsproblem des Admin-Servers
|
||||
if not exc.answered and (fresh or (exc.dns and not strict)):
|
||||
warning = True
|
||||
log(f"⚠ {exc}")
|
||||
log("⚠ Container und Ingress sind angelegt, aber die Adresse antwortet noch nicht (Zertifikat/DNS?). "
|
||||
"Später „Health erneut prüfen“ (Deploy-Tab) oder „Läuft?“ (Übersicht).")
|
||||
if exc.dns:
|
||||
log("⚠ Der Admin-Server kann den Hostnamen nicht auflösen (Resolver-Cache/DNS) – der Deploy selbst ist geschrieben. "
|
||||
"Prüfe im Browser, ob die Adresse läuft, und später „Health erneut prüfen“.")
|
||||
else:
|
||||
log("⚠ Container und Ingress sind angelegt, aber die Adresse antwortet noch nicht (Zertifikat/DNS?). "
|
||||
"Später „Health erneut prüfen“ (Deploy-Tab) oder „Läuft?“ (Übersicht).")
|
||||
else:
|
||||
raise
|
||||
log("✔ Fertig" if live and not warning else ("Fertig mit Warnung" if warning else "Dry-Run beendet – es wurde nichts geschrieben."))
|
||||
@@ -347,7 +352,7 @@ def run_sequence(tenant_ids: list[str], kind: str, actor: str, *, batch: str, co
|
||||
continue
|
||||
with session() as db:
|
||||
db.get(Deployment, did).status = "running"
|
||||
ok = execute(did, tid, kind, http, oidc_client, core_tag, health)
|
||||
ok = execute(did, tid, kind, http, oidc_client, core_tag, health, strict=bool(core_tag))
|
||||
_finish(did, "warning" if ok == "warning" else ("success" if ok else "failed"))
|
||||
results[tid] = "warning" if ok == "warning" else ("success" if ok else "failed")
|
||||
failed = not ok
|
||||
|
||||
Reference in New Issue
Block a user