Skip to content

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-health oder 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
    └─────────────────────┘
SchichtSchützt vorWann läuft sie?
SpecCode-Drift (Provisioning-Templates verteilt)Compile-Zeit / load-tenant-box-spec.ts
ReconcileManual-Drift (vergessene Migrationen)Cron 03:30 UTC + Admin-Portal-Button
Smoke-TestRuntime-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:

yaml
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_transcript

Spec-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:

bash
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:

bash
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 --all

Exit-Code ist 0 bei vollem Erfolg, 1 bei Fehlschlägen — gut für CI / Skripte.


Cron-Jobs (laufen automatisch)

JobWannWas
tenant-reconcile-daily03:30 UTC täglichAlle Tenants, fixt critical Drift
tenant-smoke-monthly1. des Monats, 04:00 UTCAlle Tenants, setzt boxStatus
whisper-health-watchAlle 5 MinutenWhisper-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:

ProbePflicht?Was prüft sie?
synapse_healthGET /_matrix/client/versions im Synapse-Container
postgres_reachablepg_isready im Postgres-Container
minio_reachableGET /minio/health/live im MinIO-Container
connector_directory_presentVerzeichnis /srv/tenants/<slug>/connectors/prilog-matrix-connector existiert auf dem Host
connector_mountedVerzeichnis ist im Synapse-Container unter /modules/prilog-matrix-connector/src/ sichtbar
connector_module_importablepython3 -c "import prilog_matrix_connector.module" ohne Fehler
connector_audio_hook_presenttranscribe_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-TypBeispielAuto-Fix?
missing_componentmatrix-connector-Verzeichnis fehlt✅ ja (Tarball ziehen + extrahieren + restart)
wrong_versionmatrix-connector-module.py weicht vom aktuellen Artifact ab (Hash-Drift)✅ ja (Reinstall aus Artifact + Synapse-Restart)
wrong_versionSynapse-Image älter als Spec verlangt⚠️ nur bei pinned: false
extra_componentEs liegen User-Daten unter unbekanntem Pfad❌ nein (manuell entscheiden)
config_diffhomeserver.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:

bash
# 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 --all

Beim nächsten Reconcile-Cron-Lauf (03:30 UTC) würde dasselbe automatisch passieren.

Sicherheits-Maßnahmen vor jedem Auto-Fix:

  1. Pre-Snapshot: Tarball von /srv/tenants/<slug>/ (ohne postgres-data + minio-data + media_store) wird unter /tmp/tenant-<slug>-<timestamp>.tar.gz abgelegt. Damit kann ein Operator manuell zurückrollen wenn was schiefgeht.
  2. Lock pro tenantId — keine zwei Reconciles gleichzeitig.
  3. Sequenzielle Ausführung — Cron geht Tenants nacheinander durch, kein Host wird parallel beansprucht.
  4. Severity-Gating: Nur severity=critical wird auto-gefixt. warning wird nur ge-logged.

Häufige Drift-Szenarien

„Tenant xyz zeigt Status broken im Admin"

  1. Klick auf Details im Admin → schau in die Drift-Reports + letzten Smoke-Test-Run.
  2. Findest du connector_* als rot → Connector-Setup kaputt. Klick auf Reconcile (echt).
  3. Findest du synapse_health rot → Synapse-Container down. Im Admin auf /agents schauen + Container-Status prüfen.
  4. Findest du postgres_reachable rot → DB-Container down. Auf shared-host SSH'en und docker compose up -d im /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/prilog prüfen, in authorized_keys des Hosts
  • Container hängt — manuell docker compose down + up -d im 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.gz mü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 ausgewertet

SQL für die häufigsten Fragen:

sql
-- 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

BegriffBedeutung
Tenant-BoxDie Container-Gruppe eines Tenants (Postgres + MinIO + Synapse + Connector) auf einem Shared-Host unter /srv/tenants/<slug>/
SpecVersioniertes YAML, das definiert was eine Box haben muss
ManifestSnapshot-Eintrag pro Tenant: welche Spec-Version wurde zuletzt angewendet, was ist installiert
DriftAbweichung zwischen IST (Box auf dem Host) und SOLL (Spec). Vier Typen: missing/wrong-version/extra/config-diff
ReconcileDen Drift erkennen + (wenn severity=critical) automatisch fixen
Smoke-TestEnd-to-End-Verifikation: 6 Probes laufen, setzen boxStatus

Verwandte Konzepte