package main import ( "context" "database/sql" "errors" "fmt" "net/url" "strings" "time" ) func ensurePostgresDatabaseExists(dsn string) error { dsn = strings.TrimSpace(dsn) if dsn == "" { return errors.New("databaseUrl ist leer") } u, err := url.Parse(dsn) if err != nil { return fmt.Errorf("databaseUrl ungueltig: %w", err) } dbName, err := postgresDatabaseNameFromURL(u) if err != nil { return err } if err := pingPostgresDSN(u.String()); err == nil { return nil } else if !isMissingPostgresDatabaseError(err) { return err } var lastErr error for _, maintenanceDB := range []string{"postgres", "template1"} { if strings.EqualFold(maintenanceDB, dbName) { continue } maintenanceURL := *u maintenanceURL.Path = "/" + maintenanceDB maintenanceURL.RawPath = "" if err := createPostgresDatabaseIfMissing(maintenanceURL.String(), dbName); err != nil { lastErr = err continue } return pingPostgresDSN(u.String()) } if lastErr != nil { return lastErr } return fmt.Errorf("Datenbank %s existiert nicht und konnte nicht angelegt werden", dbName) } func postgresDatabaseNameFromURL(u *url.URL) (string, error) { if u == nil { return "", errors.New("databaseUrl ist leer") } switch strings.ToLower(strings.TrimSpace(u.Scheme)) { case "postgres", "postgresql": default: return "", errors.New("databaseUrl muss postgres:// oder postgresql:// verwenden") } dbName, err := url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/")) if err != nil { return "", fmt.Errorf("Datenbankname ungueltig: %w", err) } dbName = strings.TrimSpace(dbName) if dbName == "" { return "", errors.New("Datenbankname fehlt in databaseUrl") } if strings.ContainsRune(dbName, 0) { return "", errors.New("Datenbankname enthaelt ungueltige Zeichen") } return dbName, nil } func pingPostgresDSN(dsn string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() db, err := sql.Open("pgx", dsn) if err != nil { return err } defer db.Close() return db.PingContext(ctx) } func createPostgresDatabaseIfMissing(maintenanceDSN string, dbName string) error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() db, err := sql.Open("pgx", maintenanceDSN) if err != nil { return err } defer db.Close() if err := db.PingContext(ctx); err != nil { return err } var exists bool if err := db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname=$1)`, dbName).Scan(&exists); err != nil { return err } if exists { return nil } _, err = db.ExecContext(ctx, `CREATE DATABASE `+quotePostgresIdentifier(dbName)) if err != nil && isDuplicatePostgresDatabaseError(err) { return nil } return err } func quotePostgresIdentifier(v string) string { return `"` + strings.ReplaceAll(v, `"`, `""`) + `"` } func isMissingPostgresDatabaseError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "sqlstate 3d000") || strings.Contains(msg, "sqlstate 30000") || strings.Contains(msg, "database") && strings.Contains(msg, "does not exist") || strings.Contains(msg, "datenbank") && strings.Contains(msg, "existiert nicht") } func isDuplicatePostgresDatabaseError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "sqlstate 42p04") || strings.Contains(msg, "already exists") || strings.Contains(msg, "existiert bereits") }