main.go 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. // Command seed applies migrations and the seed file to a fresh
  2. // Postgres. Idempotent — safe to re-run.
  3. //
  4. // Usage:
  5. //
  6. // BA_POSTGRES_DSN=postgres://... BA_MIGRATIONS_DIR=./migrations \
  7. // go run ./cmd/seed
  8. //
  9. // In docker-compose this is a one-shot sidecar that runs before
  10. // the app services start.
  11. package main
  12. import (
  13. "context"
  14. "fmt"
  15. "os"
  16. "path/filepath"
  17. "sort"
  18. "strings"
  19. "time"
  20. "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
  21. )
  22. func main() {
  23. dsn := os.Getenv("BA_POSTGRES_DSN")
  24. if dsn == "" {
  25. die("BA_POSTGRES_DSN is required")
  26. }
  27. dir := os.Getenv("BA_MIGRATIONS_DIR")
  28. if dir == "" {
  29. die("BA_MIGRATIONS_DIR is required")
  30. }
  31. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  32. defer cancel()
  33. pool, err := postgres.Connect(ctx, dsn)
  34. if err != nil {
  35. die("connect: " + err.Error())
  36. }
  37. defer pool.Close()
  38. if err := applyDir(ctx, pool, dir, "*.up.sql"); err != nil {
  39. die("apply migrations: " + err.Error())
  40. }
  41. seed := filepath.Join(dir, "seed.sql")
  42. if _, err := os.Stat(seed); err == nil {
  43. if err := execFile(ctx, pool, seed); err != nil {
  44. die("seed: " + err.Error())
  45. }
  46. fmt.Fprintln(os.Stderr, "seed applied:", seed)
  47. } else {
  48. fmt.Fprintln(os.Stderr, "no seed.sql in", dir)
  49. }
  50. fmt.Fprintln(os.Stderr, "ok")
  51. }
  52. func applyDir(ctx context.Context, pool *postgres.Pool, dir, pattern string) error {
  53. ups, err := filepath.Glob(filepath.Join(dir, pattern))
  54. if err != nil {
  55. return err
  56. }
  57. sort.Strings(ups)
  58. for _, p := range ups {
  59. if err := execFile(ctx, pool, p); err != nil {
  60. return fmt.Errorf("%s: %w", filepath.Base(p), err)
  61. }
  62. fmt.Fprintln(os.Stderr, "applied:", filepath.Base(p))
  63. }
  64. return nil
  65. }
  66. func execFile(ctx context.Context, pool *postgres.Pool, path string) error {
  67. body, err := os.ReadFile(path)
  68. if err != nil {
  69. return err
  70. }
  71. // pgx's Pool.Exec supports multi-statement SQL when the
  72. // underlying connection's protocol-mode supports it. To be
  73. // safe we acquire a single connection and exec the whole file
  74. // as one batch. If the file contains a CREATE EXTENSION that
  75. // requires superuser, the migration container runs as superuser
  76. // so this is fine.
  77. conn, err := pool.Acquire(ctx)
  78. if err != nil {
  79. return fmt.Errorf("acquire conn: %w", err)
  80. }
  81. defer conn.Release()
  82. _, err = conn.Exec(ctx, string(body))
  83. return err
  84. }
  85. func die(msg string) {
  86. fmt.Fprintln(os.Stderr, "seed:", msg)
  87. os.Exit(1)
  88. }
  89. // _ silences unused import for build tags.
  90. var _ = strings.TrimSpace