package tenant import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) // Provisioner legt fuer jeden neuen Mandanten eine vollstaendig isolierte // PostgreSQL-Datenbank an und registriert sie transaktional in der Registry // (Akzeptanzkriterium 2). Zwei Mandanten-Datenbanken sind danach auf // Infrastrukturebene komplett getrennt (Akzeptanzkriterium 3). type Provisioner struct { // adminPool ist mit der Wartungsdatenbank (z. B. "postgres") verbunden // und wird ausschliesslich fuer CREATE/DROP DATABASE verwendet, da diese // Befehle in PostgreSQL nicht in einer Transaktion laufen koennen. adminPool *pgxpool.Pool registry *Registry // dsnTemplate enthaelt genau ein "%s" als Platzhalter fuer den // Datenbanknamen, z. B. "postgresql://user:pass@host:5432/%s?sslmode=disable". dsnTemplate string } func NewProvisioner(adminPool *pgxpool.Pool, registry *Registry, dsnTemplate string) *Provisioner { return &Provisioner{adminPool: adminPool, registry: registry, dsnTemplate: dsnTemplate} } // Provision legt die Tenant-Datenbank an und registriert sie. Schlaegt die // Registrierung fehl, wird die bereits angelegte Datenbank wieder entfernt, // damit kein verwaister, unregistrierter Tenant zurueckbleibt. func (p *Provisioner) Provision(ctx context.Context, slug, name string) (Tenant, error) { if err := ValidateSlug(slug); err != nil { return Tenant{}, err } dbName := dbNameForSlug(slug) // CREATE DATABASE erlaubt keine Parameter-Platzhalter; slug ist durch // ValidateSlug bereits auf [a-z0-9_] beschraenkt, Injektion ausgeschlossen. if _, err := p.adminPool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE %q`, dbName)); err != nil { return Tenant{}, fmt.Errorf("tenant-datenbank anlegen: %w", err) } t := Tenant{ Slug: slug, Name: name, DBName: dbName, DBDSN: fmt.Sprintf(p.dsnTemplate, dbName), Status: StatusActive, } tx, err := p.registry.pool.Begin(ctx) if err != nil { p.rollbackDatabase(ctx, dbName) return Tenant{}, fmt.Errorf("registry-transaktion starten: %w", err) } created, err := p.registry.insertTx(ctx, tx, t) if err != nil { _ = tx.Rollback(ctx) p.rollbackDatabase(ctx, dbName) return Tenant{}, err } if err := tx.Commit(ctx); err != nil { p.rollbackDatabase(ctx, dbName) return Tenant{}, fmt.Errorf("registry-transaktion committen: %w", err) } return created, nil } // rollbackDatabase entfernt eine bereits angelegte Tenant-Datenbank, wenn die // Registrierung fehlschlug, damit Provisioning insgesamt atomar wirkt. func (p *Provisioner) rollbackDatabase(ctx context.Context, dbName string) { _, _ = p.adminPool.Exec(ctx, fmt.Sprintf(`DROP DATABASE IF EXISTS %q`, dbName)) }