"""Vorflug-Check: ist ein Tenant bereit für einen Live-Deploy? Liest nur (Mittwald-GET, Login-API-GET, eigener Health) und schreibt nichts. Jeder Punkt: {id, level: ok|warn|fail|info, label, detail, fix}. Ein Deploy ist „bereit“, wenn es kein `fail` gibt. """ from __future__ import annotations import socket from sqlalchemy import select from app.db import session from app.models import Secret, Tenant from app.services import contract, deploy, guard, onyva_login, preview, store from app.services.mittwald import DeployError, DryRunSkip, Mittwald def _item(id_, level, label, detail="", fix=""): return {"id": id_, "level": level, "label": label, "detail": detail, "fix": fix} def run(tenant_id: str, *, mw_http=None, oidc_client=None, self_get=None) -> dict: out: list[dict] = [] add = lambda *a, **k: out.append(_item(*a, **k)) # noqa: E731 with session() as db: t = db.get(Tenant, tenant_id) cfg, _ = store.content(db, t, "current") draft, source, pin = bool(t.draft), t.source, t.core_pin base_domain, project, stack = deploy.setting(db, "base_domain"), deploy.setting(db, "mittwald_project_id"), deploy.setting(db, "mittwald_stack_id") registry, admin_url = deploy.setting(db, "registry"), deploy.setting(db, "admin_public_url") bundle_ok = deploy.setting(db, "core_supports_bundle", "false") == "true" tag = deploy.effective_tag(db, t) host = deploy.host_for(db, t) missing = deploy.missing_secrets(db, t) oc = onyva_login.config_of(t) has_oidc_secrets = all(db.scalar(select(Secret).where(Secret.tenant_id == tenant_id, Secret.key == k, Secret.value_enc.is_not(None))) is not None for k in ("OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET")) stt_provider = (cfg.get("stt") or {}).get("provider") or "mistral" # Freigaben / Modus add("live", "ok" if deploy.live_enabled() else "warn", "DEPLOY_LIVE", "aktiv" if deploy.live_enabled() else "nicht aktiv – nur Dry-Run möglich", "" if deploy.live_enabled() else "Env DEPLOY_LIVE=true der Admin-Plattform setzen und neu starten (erst nach erfolgreichem Dry-Run).") add("allow", "ok" if guard.live_allowed(tenant_id) else "warn", "Live-Freigabe für diesen Tenant", f"DEPLOY_ALLOWED_TENANTS = {guard.describe()}", "" if guard.live_allowed(tenant_id) else f"Env DEPLOY_ALLOWED_TENANTS um „{tenant_id}“ ergänzen.") # Vertrag / Core add("contract", "ok" if contract.supported() else "fail", "Core-Vertrag", f"Admin-Core {contract.sha()[:10]}, Vertrag v{contract.contract_version()}", "" if contract.supported() else "Admin-Plattform aktualisieren.") hex_tag = deploy._hex_tag(tag) add("tag", "ok" if hex_tag else "warn", "Core-Image-Tag", f":{tag}" + (" (gepinnt)" if pin else ""), "" if hex_tag else "Besser den vollen Git-SHA als Pin setzen (Tenant → Übersicht → Core-Version) – dann prüft der Health-Check die Core-Version.") if source == "admin": add("bundle", "ok" if bundle_ok else "fail", "Core kann Bundle laden", "Einstellung core_supports_bundle", "" if bundle_ok else "Einstellungen → „Core kann Bundle laden“ aktivieren (erst wenn das Image den Bundle-Loader enthält).") good_url = admin_url.startswith("https://") and "<" not in admin_url add("adminurl", "ok" if good_url else "fail", "Öffentliche Admin-URL", admin_url or "nicht gesetzt", "" if good_url else "Einstellungen → „Öffentliche URL der Admin-Plattform“ (https://…): so holt der Tenant-Container sein Bundle.") if good_url: try: status = (self_get or _get)(f"{admin_url.rstrip('/')}/healthz") add("adminreach", "ok" if status == 200 else "fail", "Admin von außen erreichbar", f"GET /healthz → {status}", "" if status == 200 else "Domain/Proxy in Coolify prüfen.") except Exception as exc: # noqa: BLE001 add("adminreach", "fail", "Admin von außen erreichbar", f"{type(exc).__name__}: {str(exc)[:120]}", "Domain/Proxy in Coolify prüfen.") # Mittwald add("domain", "ok" if base_domain else "fail", "Basis-Domain", base_domain or "nicht gesetzt", "" if base_domain else "Einstellungen → Basis-Domain (= Gitea-Variable DOMAIN).") add("registry", "ok" if registry else "fail", "Registry", registry or "nicht gesetzt", "" if registry else "Einstellungen → Registry (= Gitea-Variable REGISTRY).") add("mtoken", "ok" if deploy.token_present() else "fail", "MITTWALD_API_TOKEN", "gesetzt" if deploy.token_present() else "nicht gesetzt", "" if deploy.token_present() else "Env MITTWALD_API_TOKEN der Admin-Plattform setzen (derselbe Token wie in Gitea).") add("mproject", "ok" if (project or stack) else "fail", "Mittwald PROJECT_ID / STACK_ID", f"{project or '–'} / {stack or '–'}", "" if (project or stack) else "Einstellungen → PROJECT_ID (und optional STACK_ID).") if deploy.token_present() and (project or stack): try: mw = Mittwald(__import__("os").getenv("MITTWALD_API_TOKEN", ""), __import__("os").getenv("MITTWALD_API_BASE", "https://api.mittwald.de/v2"), False, lambda m: None, mw_http) sid = mw.resolve_stack_id(project, stack) services = sorted(Mittwald.services_of(mw.api("GET", f"/stacks/{sid}/", write=False))) mine = f"app-{tenant_id}" protected = [s for s in services if s != mine] add("stack", "ok", "Mittwald-Stack lesbar", f"{len(services)} Services; bestehende bleiben unangetastet: {', '.join(protected) or '–'}") add("svc", "info", "Service dieses Tenants", f"{mine} " + ("existiert bereits (wird aktualisiert)" if mine in services else "existiert noch nicht (wird angelegt)")) except (DeployError, DryRunSkip) as exc: add("stack", "fail", "Mittwald-Stack lesbar", str(exc)[:200], "Token-Rechte, PROJECT_ID/STACK_ID prüfen.") except Exception as exc: # noqa: BLE001 add("stack", "fail", "Mittwald-Stack lesbar", f"{type(exc).__name__}: {str(exc)[:160]}", "Netzwerk/Token prüfen.") # Secrets / OIDC / STT will_create = oc["mode"] == "managed" and onyva_login.key_present() and not has_oidc_secrets real_missing = [m for m in missing if not (will_create and m in ("OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET"))] hint = "Tenant → Secrets (tenantspezifisch) bzw. Einstellungen → Geteilte Secrets (z. B. CORTECS_API_KEY)." if draft: hint += " Geprüft wird der VERÖFFENTLICHTE Stand – Änderungen im Entwurf (z. B. STT-Provider) erst veröffentlichen." add("secrets", "ok" if not real_missing else "fail", "Pflicht-Secrets", "vollständig" if not real_missing else "fehlen: " + ", ".join(real_missing), "" if not real_missing else hint) if will_create: add("oidcnew", "info", "OIDC-Client", "wird beim Deploy automatisch im Login angelegt (Client-ID/-Secret sofort gespeichert)") elif oc["mode"] != "off" and not onyva_login.key_present(): add("oidckey", "fail", "ONYVA_LOGIN_API_KEY", "nicht gesetzt – OIDC-Abgleich beim Deploy nicht möglich", "Env ONYVA_LOGIN_API_KEY setzen oder OIDC-Verwaltung auf „off“ stellen und Client manuell eintragen.") elif oc["mode"] != "off": add("oidckey", "ok", "Login-API", f"Modus {oc['mode']}") if oc["mode"] != "off" and oc["auth_mode"] == "required" and not oc["members"]: add("members", "warn", "Freigaben", "auth_mode „required“, aber keine Nutzer gepflegt – niemand kann sich anmelden", "Domain & Login → Freigaben: mindestens dich eintragen (Nutzer muss im Login existieren).") if stt_provider != "cortecs": add("stt", "info", "STT-Provider (veröffentlichter Stand)", stt_provider + " – braucht MISTRAL_API_KEY" + (" · im Entwurf evtl. schon geändert, aber noch nicht veröffentlicht" if draft else "")) # Inhalt with session() as db: t = db.get(Tenant, tenant_id) try: val = preview.run(db, t, "current", "validate") add("render", "ok" if val.get("ok") else "fail", "Veröffentlichter Stand rendert (echter Core)", "; ".join(val.get("errors", []))[:200] or "Web-UI und PDF ok", "" if val.get("ok") else "Tenant → Editor korrigieren und neu veröffentlichen.") except Exception as exc: # noqa: BLE001 add("render", "fail", "Rendering", str(exc)[-200:]) if draft: add("draft", "warn", "Unveröffentlichter Entwurf", "wird NICHT mit deployt", "Erst veröffentlichen, wenn er mit soll.") add("host", "info", "Ziel-URL", f"https://{host}", "") ready = not any(i["level"] == "fail" for i in out) return {"ready": ready, "items": out, "host": host, "tag": tag} def _get(url: str) -> int: import httpx return httpx.get(url, timeout=8, follow_redirects=True).status_code