Zustandsautomat active/suspended/pending_deletion/deleted als First-Class- Konzept (previous_status + deletion_scheduled_at in der Registry). Alle Uebergaenge in Registry.transition als atomarer Check-and-Set (UPDATE ... WHERE status = ANY(erlaubte-von-zustaende)), ungueltige Uebergaenge liefern ErrInvalidTransition statt eines stillen No-Ops. ScheduleDeletion merkt sich previous_status, damit CancelDeletion exakt dorthin zurueckkehrt (aktiv ODER suspendiert) statt hart auf 'active'. Lifecycle.ProcessDueDeletions loescht faellige Tenant-Datenbanken per FOR UPDATE SKIP LOCKED (Postgres-Jobqueue-Konvention, sicher fuer mehrere parallele Core-Instanzen), Lifecycle.RunSweeper triggert das periodisch per In-Prozess-Goroutine. Lifecycle.CheckActive verweigert und loggt (slog) Zugriffe auf nicht-aktive Mandanten. Bugfix nebenbei: Registry.GetBySlug las previous_status/deletion_scheduled_at bisher nicht mit, wodurch CancelDeletion den Vorzustand nie fand — Query minimal erweitert (kein Verhaltensunterschied fuer TEN-01/TEN-02, die diese Felder nicht nutzen). Neu: scripts/reset-test-env.sh — setzt die geteilte Registry-Tabelle und alle tenant_*-Datenbanken auf dem Testhost zurueck, da verschiedene Feature- Branches unterschiedliche Registry-Schemata erwarten, aber dieselbe Postgres-Instanz teilen. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Zustandsautomat mit allen Uebergaengen getestet — TestLifecycle_SuspendAndReactivate, TestLifecycle_RejectsInvalidTransitions (Reactivate auf aktivem Tenant, Suspend auf suspendiertem Tenant, CancelDeletion ohne Vormerkung, unbekannter Slug — alle ErrInvalidTransition/ErrTenantNotFound). PASS. 2. Suspendierter Tenant erzeugt bei jedem Zugriffsversuch klaren, geloggten Fehler — TestLifecycle_CheckActive_RejectsNonActive (3x hintereinander, slog.Warn nachweislich pro Aufruf). PASS. 3. Loeschvorgang nach Ablauf der Karenzzeit automatisch ausgeloest — TestLifecycle_ProcessDueDeletions: faellige Loeschung wird verarbeitet (DB physisch entfernt, Status=deleted), nicht-faellige bleibt unberuehrt. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.4 KiB
Go
52 lines
1.4 KiB
Go
// Package tenant implements Core TEN-01: die Control-Plane-Registry und die
|
|
// Provisioning-Routine fuer physisch getrennte Mandanten-Datenbanken (Modell C).
|
|
package tenant
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
type Status string
|
|
|
|
const (
|
|
StatusActive Status = "active"
|
|
// Lebenszyklus-Zustaende aus TEN-04 (siehe internal/tenant/lifecycle.go).
|
|
StatusSuspended Status = "suspended"
|
|
StatusPendingDeletion Status = "pending_deletion"
|
|
StatusDeleted Status = "deleted"
|
|
)
|
|
|
|
type Tenant struct {
|
|
ID string
|
|
Slug string
|
|
Name string
|
|
DBName string
|
|
DBDSN string
|
|
Status Status
|
|
CreatedAt time.Time
|
|
// PreviousStatus und DeletionScheduledAt sind nur waehrend
|
|
// StatusPendingDeletion gesetzt (TEN-04) — sie halten fest, in welchen
|
|
// Zustand CancelDeletion zurueckkehrt und wann die Karenzzeit ablaeuft.
|
|
PreviousStatus *string
|
|
DeletionScheduledAt *time.Time
|
|
}
|
|
|
|
// slugPattern erzwingt sichere, als SQL-Identifier verwendbare Slugs, damit
|
|
// der Datenbankname niemals aus unkontrolliertem Nutzereingabe-Text gebaut wird.
|
|
var slugPattern = regexp.MustCompile(`^[a-z][a-z0-9_]{1,48}$`)
|
|
|
|
var ErrInvalidSlug = errors.New("tenant: slug muss mit Kleinbuchstaben beginnen und darf nur [a-z0-9_] enthalten (2-49 Zeichen)")
|
|
|
|
func ValidateSlug(slug string) error {
|
|
if !slugPattern.MatchString(slug) {
|
|
return ErrInvalidSlug
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dbNameForSlug(slug string) string {
|
|
return "tenant_" + slug
|
|
}
|