Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / cmd/migrate-all-tenants/main.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 |
| Branch | feature/aioe-upgrade |
| Path or archive member | cmd/migrate-all-tenants/main.go |
| Original SHA-256 | 6342d6fc0912c24267237b52df2c406dee8d3efa872c56b305aaee8f6d82de32 |
| Git blob SHA | 77ecd8cfcc1d5cdeb0c073c39d56ce44376d9e9b |
| Reading-copy SHA-256 | 6342d6fc0912c24267237b52df2c406dee8d3efa872c56b305aaee8f6d82de32 |
Every recorded occurrence
These 5 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / cmd/migrate-all-tenants/main.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / cmd/migrate-all-tenants/main.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / cmd/migrate-all-tenants/main.go | deploy-and-ops | bdacd9d0b4d5998d25fc74bf4007f4c3ec538c7a | Open original |
| Janady13/missionmatch / cmd/migrate-all-tenants/main.go | per-tenant-routing | 9bde0f9a74b89d01980fa658e5253acda7528d0c | Open original |
| Janady13/missionmatch / cmd/migrate-all-tenants/main.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓268 lines · 9.1 KB
// Command migrate-all-tenants applies the migration set to the control plane database AND to every active
// tenant database in the database per tenant model. It reads the tenant directory in the control plane to
// discover the tenant databases, then for each target applies BOTH the public schema migrations and the
// segregated eeo migrations (two separate goose version tables), so a tenant database can never drift one
// schema relative to the other.
//
// It is a migrator role operator tool (it needs BYPASSRLS to write the directory bookkeeping and to run
// DDL). It is idempotent and resumable: goose records applied versions, so a re run is a no op on a
// healthy database. The failure surface is hard: it records which database errored or is behind, keeps
// going to the rest, and exits non zero if ANY database failed or is still behind head, so a partial fan
// out can never report success.
//
// Two modes:
//
// migrate-all-tenants apply pending migrations to the control plane and every active tenant
// migrate-all-tenants -check dry run: report each database's version and whether it is behind, NO writes
//
// It uses the per instance goose Provider (NOT the process global goose API, whose SetBaseFS and
// SetTableName state would race and cross contaminate the public versus eeo table name across N databases
// and under concurrency, the pressly/goose multi database guidance), and a Postgres session locker so two
// operators (or a concurrent provision-tenant) cannot migrate the same database at once.
package main
import (
"context"
"database/sql"
"flag"
"fmt"
"io/fs"
"log"
"os"
"sort"
"sync"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
goose "github.com/pressly/goose/v3"
goosedb "github.com/pressly/goose/v3/database"
gooselock "github.com/pressly/goose/v3/lock"
"golang.org/x/sync/errgroup"
dbembed "github.com/missionworkforce/missionmatch/db"
"github.com/missionworkforce/missionmatch/internal/platform/database"
"github.com/missionworkforce/missionmatch/internal/platform/directory"
)
type target struct {
name string // human label (control plane, or the tenant database name)
dsn string // migrator DSN to this database
org *uuid.UUID // nil for the control plane; set for a tenant (to record bookkeeping)
}
type result struct {
name string
pubVersion int64
eeoVersion int64
pending bool // dry run: any pending migration
appliedPub int
appliedEEO int
err error
}
func main() {
controlDSN := flag.String("control-dsn", os.Getenv("MM_MIGRATOR_DATABASE_URL"), "migrator DSN to the control plane database (defaults to MM_MIGRATOR_DATABASE_URL)")
check := flag.Bool("check", false, "dry run: report each database's version and whether it is behind, make no changes")
concurrency := flag.Int("concurrency", 1, "migrate up to N databases at once (distinct databases only)")
flag.Parse()
if *controlDSN == "" {
log.Fatal("migrate-all-tenants: -control-dsn (or MM_MIGRATOR_DATABASE_URL) is required")
}
if *concurrency < 1 {
*concurrency = 1
}
ctx := context.Background()
// The control plane pool: it must be the BYPASSRLS migrator so it can read the directory and run DDL.
pool, err := pgxpool.New(ctx, *controlDSN)
if err != nil {
log.Fatalf("migrate-all-tenants: control plane pool: %v", err)
}
defer pool.Close()
if err := database.AssertMigratorRole(ctx, pool); err != nil {
log.Fatalf("migrate-all-tenants: %v", err)
}
dir := directory.New(pool)
// Targets: the control plane first (the foundation), then every active tenant database. A tenant DSN
// reuses the migrator credential and swaps only the database name (the migrator is a cluster role).
targets := []target{{name: "control plane", dsn: *controlDSN, org: nil}}
tenants, err := dir.List(ctx, directory.StatusActive)
if err != nil {
log.Fatalf("migrate-all-tenants: list tenants: %v", err)
}
for _, t := range tenants {
org := t.OrganizationID
targets = append(targets, target{name: t.DBName, dsn: swapDB(*controlDSN, t.DBName), org: &org})
}
log.Printf("migrate-all-tenants: %d targets (control plane + %d active tenants), check=%v concurrency=%d", len(targets), len(tenants), *check, *concurrency)
results := make([]result, len(targets))
var g errgroup.Group
g.SetLimit(*concurrency)
var mu sync.Mutex // guards the directory bookkeeping writes
for i := range targets {
i := i
g.Go(func() error {
r := migrateOne(ctx, targets[i], *check)
results[i] = r
// Record the applied version in the directory for a tenant that advanced (apply mode only).
if !*check && r.err == nil && targets[i].org != nil {
mu.Lock()
if e := dir.SetMigrated(ctx, *targets[i].org, r.pubVersion); e != nil {
log.Printf("migrate-all-tenants: WARNING could not record migration bookkeeping for %s: %v", targets[i].name, e)
}
mu.Unlock()
}
return nil // never abort the group on one database; the summary and exit code carry the verdict
})
}
_ = g.Wait()
// Summary and a hard exit code.
failed, behind := report(results, *check)
if failed > 0 || (*check && behind > 0) {
os.Exit(1)
}
}
// migrateOne applies (or, in check mode, inspects) the two migration sets on one database using per
// instance goose Providers and a session lock.
func migrateOne(ctx context.Context, t target, check bool) result {
r := result{name: t.name}
sqlDB, err := sql.Open("pgx", t.dsn)
if err != nil {
r.err = fmt.Errorf("open: %w", err)
return r
}
defer func() { _ = sqlDB.Close() }()
sqlDB.SetMaxOpenConns(5)
sqlDB.SetConnMaxLifetime(5 * time.Minute)
locker, err := gooselock.NewPostgresSessionLocker()
if err != nil {
r.err = fmt.Errorf("locker: %w", err)
return r
}
pubFS, err := fs.Sub(dbembed.Migrations, "migrations")
if err != nil {
r.err = err
return r
}
pub, err := goose.NewProvider(goose.DialectPostgres, sqlDB, pubFS, goose.WithSessionLocker(locker))
if err != nil {
r.err = fmt.Errorf("public provider: %w", err)
return r
}
defer func() { _ = pub.Close() }()
eeoStore, err := goosedb.NewStore(goosedb.DialectPostgres, "goose_db_version_eeo")
if err != nil {
r.err = fmt.Errorf("eeo store: %w", err)
return r
}
eeoFS, err := fs.Sub(dbembed.EEOMigrations, "migrations_eeo")
if err != nil {
r.err = err
return r
}
// Empty dialect because the table name is carried by the custom store.
eeo, err := goose.NewProvider("", sqlDB, eeoFS, goose.WithStore(eeoStore), goose.WithSessionLocker(locker))
if err != nil {
r.err = fmt.Errorf("eeo provider: %w", err)
return r
}
defer func() { _ = eeo.Close() }()
if check {
pubPending, e1 := pub.HasPending(ctx)
eeoPending, e2 := eeo.HasPending(ctx)
if e1 != nil || e2 != nil {
r.err = firstErr(e1, e2)
return r
}
r.pending = pubPending || eeoPending
r.pubVersion, _ = pub.GetDBVersion(ctx)
r.eeoVersion, _ = eeo.GetDBVersion(ctx)
return r
}
// Apply public then eeo, so the public foundation exists before the segregated eeo schema.
rp, err := pub.Up(ctx)
if err != nil {
r.err = fmt.Errorf("public migrations: %w", err)
return r
}
re, err := eeo.Up(ctx)
if err != nil {
r.err = fmt.Errorf("eeo migrations: %w", err)
return r
}
r.appliedPub, r.appliedEEO = len(rp), len(re)
r.pubVersion, _ = pub.GetDBVersion(ctx)
r.eeoVersion, _ = eeo.GetDBVersion(ctx)
return r
}
// report prints a per database table and returns how many failed and how many are behind.
func report(results []result, check bool) (failed, behind int) {
sorted := append([]result(nil), results...)
sort.SliceStable(sorted, func(i, j int) bool { return sorted[i].name < sorted[j].name })
log.Printf("migrate-all-tenants: results")
for _, r := range sorted {
switch {
case r.err != nil:
failed++
log.Printf(" FAILED %-28s public=v%d eeo=v%d %v", r.name, r.pubVersion, r.eeoVersion, r.err)
case check && r.pending:
behind++
log.Printf(" BEHIND %-28s public=v%d eeo=v%d (has pending migrations)", r.name, r.pubVersion, r.eeoVersion)
case check:
log.Printf(" OK %-28s public=v%d eeo=v%d (at head)", r.name, r.pubVersion, r.eeoVersion)
default:
log.Printf(" APPLIED %-28s public=v%d (+%d) eeo=v%d (+%d)", r.name, r.pubVersion, r.appliedPub, r.eeoVersion, r.appliedEEO)
}
}
if failed > 0 {
log.Printf("migrate-all-tenants: %d database(s) FAILED", failed)
}
if check && behind > 0 {
log.Printf("migrate-all-tenants: %d database(s) BEHIND head", behind)
}
if failed == 0 && behind == 0 {
log.Printf("migrate-all-tenants: all databases OK")
}
return failed, behind
}
func firstErr(errs ...error) error {
for _, e := range errs {
if e != nil {
return e
}
}
return nil
}
// swapDB reuses a migrator DSN and swaps only the database name, so a tenant database is reached with the
// same migrator credential (the migrator is a cluster role). Mirrors the helper in cmd/provision-tenant.
func swapDB(dsn, name string) string {
cfg, err := pgx.ParseConfig(dsn)
if err != nil {
return dsn
}
out := fmt.Sprintf("host=%s port=%d user=%s dbname=%s", cfg.Host, cfg.Port, cfg.User, name)
if cfg.Password != "" {
out += " password=" + cfg.Password
}
if cfg.TLSConfig == nil {
out += " sslmode=disable"
}
return out
}