Tenant-Spec, Reconcile & Smoke-Test
Wie wir verhindern, dass uns je wieder ein Tenant fehlt was er haben soll. Drei-Schichten-Schutz: deklarative Spec (was es heißt „komplett zu sein"), tägliche Reconcile (Drift-Detection + Auto-Fix), monatliche Smoke-Tests (End-to-End-Verifikation).
Wann ist das relevant?
- Operator der einen neuen Tenant aufsetzt → Spec wird automatisch angewendet
- Operator der einen Live-Tenant prüfen will → Admin-Portal
/tenant-healthoder CLI - Bei einem Vorfall (User meldet „Funktion X geht nicht") → Smoke-Test gibt sofort eine Liste der defekten Komponenten
Konzept-Details + Datenmodell: tenant-spec-reconcile-konzept.
Was ist das Problem das gelöst wurde?
Bis Mai 2026 war „was ein vollständiger Prilog-Tenant ist" nicht zentral definiert. Die Information lag verteilt in:
- Provisioning-Templates (
shared-hosting.service.ts) - Agent-Provision-Steps (
prilog-agent) - Manuellen SSH-Schritten (z.B. „Connector via tar+restart ausrollen")
- Memory-Files / Konzept-Doks
Bei der Tenant-in-a-Box-Migration (1.5.2026) ist der Matrix-Connector dabei vergessen worden. Niemand merkte es bis am 10.5. ein User Flurfunk-Sprachnachrichten testen wollte und nichts passierte.
Das ist Provisioning-Drift. Ohne zentrale Definition + automatische Korrektur passiert das immer wieder bei Architektur-Schritten.
Die drei Schichten im Überblick
┌────────────────────────┐
│ Tenant-Box-Spec │ YAML in /opt/prilog/specs/
│ (deklarativ, vers.) │ 1 Datei = 1 SOLL-Zustand
└────────────┬───────────┘
│
┌─────┴──────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Provisioning │ │ Reconcile-Cron │ täglich 03:30 UTC
│ (neue Boxen) │ │ + manuell │ IST gegen SOLL, fixt critical
└──────────────┘ └──────────────────┘
│ │
└──────┬───────┘
▼
┌─────────────────────┐
│ Smoke-Test │ monatlich + on demand
│ End-to-End │ 6 Probes, setzt boxStatus
└─────────────────────┘| Schicht | Schützt vor | Wann läuft sie? |
|---|---|---|
| Spec | Code-Drift (Provisioning-Templates verteilt) | Compile-Zeit / load-tenant-box-spec.ts |
| Reconcile | Manual-Drift (vergessene Migrationen) | Cron 03:30 UTC + Admin-Portal-Button |
| Smoke-Test | Runtime-Drift (Container läuft, Modul tot) | Cron monatlich + Admin-Portal-Button |
Die Spec (deklaratives YAML)
Jede Spec ist eine versionierte Datei. Beispiel — die aktuell aktive v4:
schema_version: 4
storage:
base_path: /srv/tenants/{slug}
required_dirs:
- postgres
- minio
- synapse/media_store
- connectors/prilog-matrix-connector
components:
- name: postgres
type: container
image: postgres:16-alpine
container_name: pg-{slug}
pinned: true # bei Update keine Auto-Bump
healthcheck: { type: pg_isready, interval_seconds: 10 }
severity: critical
- name: minio
type: container
image: minio/minio:RELEASE.2024-12-13T22-19-12Z
pinned: true
severity: critical
- name: synapse
type: container
image: matrixdotorg/synapse:v1.124.0
homeserver_yaml_modules:
- module: prilog_matrix_connector.module.PrilogMatrixConnectorModule
severity: critical
- name: matrix-connector
type: synapse_module
artifact:
source: /var/www/prilog-artifacts/matrix-connector/prilog-matrix-connector-latest.tar.gz
extract_to: /srv/tenants/{slug}/connectors/prilog-matrix-connector
severity: critical
required_components:
- postgres
- minio
- synapse
- matrix-connector
post_provision:
- smoke_test: synapse_health
- smoke_test: send_test_audio_and_expect_transcriptSpec-Versionen sind unveränderlich. Wenn sich was ändert, muss eine neue Version (v5, v6, ...) angelegt werden. Damit garantiert der contentHash (SHA-256 über kanonisches JSON), dass wir Drift gegenüber einer bekannten Version messen können.
Spec laden + aktivieren
Die Spec lebt als YAML im Repo unter prilog-backend-api/specs/tenant-box-vN.yaml. Eine neue Version wird in die DB gespielt mit:
ssh -i ~/.ssh/prilog lee@91.99.30.243
cd /var/www/backend-api
npx tsx prisma/load-tenant-box-spec.ts specs/tenant-box-v5.yaml \
--notes "v5 fügt redis-Cache als Pflicht-Komponente hinzu"Idempotent: gleicher Inhalt → kein Re-Insert. Geänderter Inhalt mit gleicher schema_version → Fehler (Versions-Bump-Pflicht). Damit kann man eine Spec nicht „heimlich umschreiben".
Die aktive Spec ist immer die höchste Versionsnummer in der DB.
Im Admin-Portal nutzen
admin.prilog.chat → Sidebar → Tenant-Health (Spec).
Was du dort siehst:
- Banner mit der aktuell aktiven Spec-Version + Hash + Notes
- Tabelle aller aktiven Tenants, sortiert nach
boxStatus:- 🟢 OK — letzter Smoke-Test grün
- 🟡 Degraded — optionale Probe rot, nutzbar
- 🔴 Broken — Pflicht-Probe rot
- ⚪ Pending — nie getestet
- Pro Tenant Buttons:
- Dry — Reconcile-Run nur lesend, zeigt Drift ohne zu fixen
- Reconcile — fixt automatisch, was fixbar ist
- Smoke — End-to-End-Test (~3-5 s)
- Details — Drift-Reports + letzte 10 Smoke-Test-Runs
Reconcile ist nicht harmlos
Ein Reconcile-Run startet Container neu. Im normalen Fall ist das ohne Ausfall (graceful), aber bei laufenden Schul-Stoßzeiten lieber außerhalb von 8-15 Uhr planen.
CLI
Ohne Admin-Portal direkt vom api-Server:
ssh -i ~/.ssh/prilog lee@91.99.30.243
cd /var/www/backend-api
# Spec verwalten
npx tsx prisma/load-tenant-box-spec.ts specs/tenant-box-v4.yaml
# Reconcile
npx tsx scripts/reconcile-tenant.ts --tenant=leander # echter Run
npx tsx scripts/reconcile-tenant.ts --tenant=leander --dry-run # nur lesen
npx tsx scripts/reconcile-tenant.ts --all # alle Tenants
npx tsx scripts/reconcile-tenant.ts --all --dry-run
# Smoke-Test
npx tsx scripts/smoke-test-tenant.ts --tenant=leander
npx tsx scripts/smoke-test-tenant.ts --allExit-Code ist 0 bei vollem Erfolg, 1 bei Fehlschlägen — gut für CI / Skripte.
Cron-Jobs (laufen automatisch)
| Job | Wann | Was |
|---|---|---|
tenant-reconcile-daily | 03:30 UTC täglich | Alle Tenants, fixt critical Drift |
tenant-smoke-monthly | 1. des Monats, 04:00 UTC | Alle Tenants, setzt boxStatus |
whisper-health-watch | Alle 5 Minuten | Whisper-Server-Health (separat) |
Status der Cron-Läufe in /crons im Admin-Portal.
Die 7 Smoke-Test-Probes
Pro Tenant werden diese sieben Probes nacheinander ausgeführt:
| Probe | Pflicht? | Was prüft sie? |
|---|---|---|
synapse_health | ✅ | GET /_matrix/client/versions im Synapse-Container |
postgres_reachable | ✅ | pg_isready im Postgres-Container |
minio_reachable | ✅ | GET /minio/health/live im MinIO-Container |
connector_directory_present | ✅ | Verzeichnis /srv/tenants/<slug>/connectors/prilog-matrix-connector existiert auf dem Host |
connector_mounted | ✅ | Verzeichnis ist im Synapse-Container unter /modules/prilog-matrix-connector/src/ sichtbar |
connector_module_importable | ✅ | python3 -c "import prilog_matrix_connector.module" ohne Fehler |
connector_audio_hook_present | ✅ | transcribe_voice ist im ausgelieferten module.py referenziert |
Warum die siebte Probe?
Bis zum 10.5.2026 lieferten alle drei Tenant-Boxen einen alten Connector-Stand vom 21.3. aus — importierbar war er, aktuell nicht. Der Audio-Hook (transcribe_voice) fehlte. Flurfunk-Aufnahmen liefen ins Client-Timeout. Die connector_audio_hook_present-Probe deckt genau diesen Drift-Typ.
Auswertung:
- Alle Probes ok →
status='ok' - Eine Pflicht-Probe rot →
status='failed'(Tenant ist „broken") - Optionale Probe rot →
status='degraded'
Tenant.boxStatus und boxStatusReason werden nach jedem Smoke-Test entsprechend gesetzt.
Drift-Typen + Auto-Fix-Regeln
| Drift-Typ | Beispiel | Auto-Fix? |
|---|---|---|
missing_component | matrix-connector-Verzeichnis fehlt | ✅ ja (Tarball ziehen + extrahieren + restart) |
wrong_version | matrix-connector-module.py weicht vom aktuellen Artifact ab (Hash-Drift) | ✅ ja (Reinstall aus Artifact + Synapse-Restart) |
wrong_version | Synapse-Image älter als Spec verlangt | ⚠️ nur bei pinned: false |
extra_component | Es liegen User-Daten unter unbekanntem Pfad | ❌ nein (manuell entscheiden) |
config_diff | homeserver.yaml fehlt der modules:-Block | ⚠️ nur bei deklarierter auto_fix: true-Markierung |
Connector-Hash-Drift (seit 10.5.2026)
Der Reconcile-Inspector liest den sha256 der ausgelieferten module.py mit. Ein interner Helper (connector-artifact.ts) extrahiert den Soll-Hash aus dem aktuellen Tarball unter /var/www/prilog-artifacts/matrix-connector/prilog-matrix-connector-latest.tar.gz (cached 60 s, USTAR-Header direkt geparst). Bei Hash-Mismatch wird wrong_version-Drift gemeldet und die Pipeline reagiert mit installMatrixConnector + Synapse-Restart — der Restart ist nötig, weil der Container den Connector-Code beim Start in den Speicher liest.
Workflow nach Connector-Code-Änderung:
# 1. Im Connector-Repo: aktuelles Tarball bauen
cd /home/lee/prilog-matrix-connector
python3 scripts/build_artifact.py
# 2. Nach prilog-artifacts/ kopieren (lokal + auf api.prilog.chat)
HASH=$(git rev-parse --short HEAD)
cp dist/prilog-matrix-connector.tar.gz /home/lee/prilog-artifacts/matrix-connector/prilog-matrix-connector-${HASH}.tar.gz
cp dist/prilog-matrix-connector.tar.gz /home/lee/prilog-artifacts/matrix-connector/prilog-matrix-connector-latest.tar.gz
scp -i ~/.ssh/prilog /home/lee/prilog-artifacts/matrix-connector/*.tar.gz lee@api.prilog.chat:/var/www/prilog-artifacts/matrix-connector/
# 3. Reconcile aller Tenants triggert die Reinstall-Welle
npx tsx scripts/reconcile-tenant.ts --allBeim nächsten Reconcile-Cron-Lauf (03:30 UTC) würde dasselbe automatisch passieren.
Sicherheits-Maßnahmen vor jedem Auto-Fix:
- Pre-Snapshot: Tarball von
/srv/tenants/<slug>/(ohnepostgres-data+minio-data+media_store) wird unter/tmp/tenant-<slug>-<timestamp>.tar.gzabgelegt. Damit kann ein Operator manuell zurückrollen wenn was schiefgeht. - Lock pro
tenantId— keine zwei Reconciles gleichzeitig. - Sequenzielle Ausführung — Cron geht Tenants nacheinander durch, kein Host wird parallel beansprucht.
- Severity-Gating: Nur
severity=criticalwird auto-gefixt.warningwird nur ge-logged.
Häufige Drift-Szenarien
„Tenant xyz zeigt Status broken im Admin"
- Klick auf Details im Admin → schau in die Drift-Reports + letzten Smoke-Test-Run.
- Findest du
connector_*als rot → Connector-Setup kaputt. Klick auf Reconcile (echt). - Findest du
synapse_healthrot → Synapse-Container down. Im Admin auf/agentsschauen + Container-Status prüfen. - Findest du
postgres_reachablerot → DB-Container down. Auf shared-host SSH'en unddocker compose up -dim/srv/tenants/<slug>/-Pfad.
„Reconcile schlägt mit fix_error fehl"
Der fix_error-Text steht im Drift-Report (Admin-Portal Detail-Panel). Häufige Ursachen:
- Artifact fehlt unter
/var/www/prilog-artifacts/...— neueres Artifact bauen und scp'en - SSH-Key ist nicht authorized auf dem Shared-Host —
~/.ssh/prilogprüfen, inauthorized_keysdes Hosts - Container hängt — manuell
docker compose down+up -dim Tenant-Pfad
„Spec-Bump v3 → v4: was passiert mit Bestand?"
Bestandstenants pinnen ihre angewendete Spec-Version in tenant_box_manifests.applied_spec_version. Reconcile vergleicht gegen die höchste Spec in der DB (nicht gegen die gepinnte). Heißt: Beim nächsten Cron-Run wird auf v4 hochgemigriert, falls Drift-Detection das verlangt.
Wenn du das gestaffelt rollen willst (z.B. erst Test-Tenants, dann Free, dann Pro), ist das aktuell manuell — pro Tenant via --dry-run prüfen + dann ohne. Eine automatisierte Rollout-Pipeline ist Backlog (siehe Konzept-Doc Sektion „Bewusst weggelassen").
„Wir wollen einen Tenant auf einer alten Spec-Version pinnen"
Aktuell nicht direkt unterstützt — Reconcile fixt immer gegen die höchste Spec-Version. Workaround: Im Schema die alte Spec als „aktuelle" deklarieren, indem man die neue erst hinzufügt wenn der ältere Tenant migriert ist. Ein dediziertes „pin"-Feature kommt später.
Was bewusst (noch) nicht automatisch passiert
- Provisioning-Code an Spec gekoppelt — neue Tenants kommen aktuell aus den Inline-Templates in
shared-hosting.service.ts. Der Reconcile-Cron fängt sie binnen 24 Stunden auf, aber der erste Spawn folgt nicht der Spec. (Backlog-Aufgabe Phase 7.) - Tier-spezifische Specs (Free/Pro/Enterprise) — heute eine Spec für alle.
- Pre-Reconcile-Snapshot-Cleanup —
/tmp/tenant-<slug>-*.tar.gzmüssen manuell aufgeräumt werden (oder warten auf den geplanten 7-Tage-Cleanup-Cron). - Rolling-Update bei Spec-Bump (gestufftes 5%/Free/Pro/Enterprise) — heute manuell über
--tenant=<slug>einzeln. - GUI-Spec-Editor im Admin-Portal — heute YAML im Repo, CLI zum Laden.
Alle diese Punkte sind als Aufgaben im Prilog-Space (leander) als Backlog-Items.
Quick-Reference Datenbank
Vier Tabellen + 3 Felder auf tenants:
tenant_box_specs — versionierte SOLL-Definition (eine Zeile pro version)
tenant_box_manifests — IST-Stand pro Tenant (eine Zeile pro Tenant)
tenant_drift_reports — eine Zeile pro detected drift (mit fix-Status)
tenant_smoke_test_runs — eine Zeile pro Smoke-Test-Lauf
tenants.box_status = 'ok' | 'degraded' | 'broken' | 'pending'
tenants.box_status_reason = letzter Fehlertext (oder NULL)
tenants.box_status_at = wann zuletzt ausgewertetSQL für die häufigsten Fragen:
-- Wieviele Tenants sind broken?
SELECT box_status, COUNT(*) FROM tenants GROUP BY box_status;
-- Welche Drift-Reports sind offen?
SELECT t.tenant_key, dr.drift_type, dr.component, dr.severity, dr.detected_at
FROM tenant_drift_reports dr JOIN tenants t ON t.id = dr.tenant_id
WHERE dr.fixed_at IS NULL ORDER BY dr.detected_at DESC LIMIT 50;
-- Letzter Smoke-Run pro Tenant
SELECT DISTINCT ON (t.tenant_key)
t.tenant_key, r.status, r.duration_ms, r.first_error, r.started_at
FROM tenant_smoke_test_runs r JOIN tenants t ON t.id = r.tenant_id
ORDER BY t.tenant_key, r.started_at DESC;Glossar
| Begriff | Bedeutung |
|---|---|
| Tenant-Box | Die Container-Gruppe eines Tenants (Postgres + MinIO + Synapse + Connector) auf einem Shared-Host unter /srv/tenants/<slug>/ |
| Spec | Versioniertes YAML, das definiert was eine Box haben muss |
| Manifest | Snapshot-Eintrag pro Tenant: welche Spec-Version wurde zuletzt angewendet, was ist installiert |
| Drift | Abweichung zwischen IST (Box auf dem Host) und SOLL (Spec). Vier Typen: missing/wrong-version/extra/config-diff |
| Reconcile | Den Drift erkennen + (wenn severity=critical) automatisch fixen |
| Smoke-Test | End-to-End-Verifikation: 6 Probes laufen, setzen boxStatus |
Verwandte Konzepte
- Tenant-in-a-Box (Architektur) — der Container-Stack pro Tenant
- Tenant-Migration & Auto-Provisioning — wie ein neuer Tenant entsteht
- Tenant-Box Update-Pipeline (Phase 6) — orthogonale Update-Strategie für Image-Bumps
- Tenant-Spec & Reconcile Konzept — vollständiges Konzept-Doc inkl. Datenmodell