86 lines
2.0 KiB
Go
86 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"felis.lolicon.best/internal/store"
|
|
)
|
|
|
|
// checkPostgres proves the configured database is reachable and accepts a
|
|
// connection. Host bootstrap provisions PostgreSQL, so in the normal setup flow
|
|
// this succeeds immediately; the preflight stage uses it to fail fast otherwise.
|
|
func checkPostgres(dbURL string) error {
|
|
cfg, err := parseDBURL(dbURL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid database URL: %w", err)
|
|
}
|
|
conn, err := net.DialTimeout("tcp", cfg.addr, 2*time.Second)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot reach PostgreSQL at %s: %w", cfg.addr, err)
|
|
}
|
|
conn.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
drv, err := store.Open(ctx, dbURL)
|
|
if err != nil {
|
|
return fmt.Errorf("connect to PostgreSQL: %w", err)
|
|
}
|
|
drv.Close()
|
|
return nil
|
|
}
|
|
|
|
type dbCfg struct {
|
|
addr string
|
|
user string
|
|
pass string
|
|
db string
|
|
}
|
|
|
|
func parseDBURL(url string) (dbCfg, error) {
|
|
// Simple parser for postgres://user:pass@host:port/db?options
|
|
s := strings.TrimPrefix(url, "postgres://")
|
|
s = strings.TrimPrefix(s, "postgresql://")
|
|
parts := strings.SplitN(s, "@", 2)
|
|
if len(parts) != 2 {
|
|
return dbCfg{}, fmt.Errorf("malformed URL")
|
|
}
|
|
auth := strings.SplitN(parts[0], ":", 2)
|
|
rest := strings.SplitN(parts[1], "/", 2)
|
|
if len(rest) < 2 {
|
|
return dbCfg{}, fmt.Errorf("malformed URL: no database")
|
|
}
|
|
hostport := rest[0]
|
|
dbname := strings.SplitN(rest[1], "?", 2)[0]
|
|
if !strings.Contains(hostport, ":") {
|
|
hostport += ":5432"
|
|
}
|
|
return dbCfg{
|
|
addr: hostport,
|
|
user: auth[0],
|
|
pass: func() string {
|
|
if len(auth) > 1 {
|
|
return auth[1]
|
|
}
|
|
return ""
|
|
}(),
|
|
db: dbname,
|
|
}, nil
|
|
}
|
|
|
|
// readSchema lines the database's recorded migrations up with this build's.
|
|
func readSchema(dbURL string) (store.Schema, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
drv, err := store.Open(ctx, dbURL)
|
|
if err != nil {
|
|
return store.Schema{}, err
|
|
}
|
|
defer drv.Close()
|
|
return store.ReadSchema(ctx, drv)
|
|
}
|