Files
gochat/backend/cmd/migrate/main.go
T
Rogeeandrogee 6d6d80dd86 HH-441: harden durable storage and recovery (#92)
* HH-441: harden durable storage and recovery

* HH-441: clear recovery review blockers

* HH-441: enforce offsite backup failure domain

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-22 02:16:02 +08:00

132 lines
3.7 KiB
Go

package main
import (
"fmt"
"os"
"strconv"
"github.com/gochat/gochat/internal/config"
"github.com/gochat/gochat/internal/database"
)
func main() {
config.LoadDotEnvEnvironment()
database.SanitizePostgresEnvironment()
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
command := os.Args[1]
// Load the same root .env and environment overlay as the application.
env := os.Getenv("GOCHAT_ENV")
if env == "" {
env = "development"
}
cfg, err := config.LoadWithEnv(env)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}
dbURL := cfg.Database.MigrateDSN()
migrationsPath := cfg.Database.GetMigrationsPath()
switch command {
case "up":
if err := database.RunMigrations(dbURL, migrationsPath); err != nil {
fmt.Fprintf(os.Stderr, "Error running migrations up: %v\n", err)
os.Exit(1)
}
fmt.Println("Migrations applied successfully (up)")
case "down":
if err := rejectDestructiveProductionMigration(env, -1); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := database.RollbackMigrations(dbURL, migrationsPath); err != nil {
fmt.Fprintf(os.Stderr, "Error running migrations down: %v\n", err)
os.Exit(1)
}
fmt.Println("Migrations rolled back successfully (down)")
case "force":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Error: 'force' requires a version argument\n")
printUsage()
os.Exit(1)
}
version, err := strconv.Atoi(os.Args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid version number '%s': %v\n", os.Args[2], err)
os.Exit(1)
}
if err := database.ForceVersion(dbURL, migrationsPath, version); err != nil {
fmt.Fprintf(os.Stderr, "Error forcing version %d: %v\n", version, err)
os.Exit(1)
}
fmt.Printf("Migration version forced to %d\n", version)
case "version":
version, dirty, err := database.CurrentVersion(dbURL, migrationsPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current version: %v\n", err)
os.Exit(1)
}
if dirty {
fmt.Printf("Current version: %d (DIRTY — a migration partially failed)\n", version)
} else {
fmt.Printf("Current version: %d\n", version)
}
case "steps":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Error: 'steps' requires a step count argument\n")
printUsage()
os.Exit(1)
}
steps, err := strconv.Atoi(os.Args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid step count '%s': %v\n", os.Args[2], err)
os.Exit(1)
}
if err := rejectDestructiveProductionMigration(env, steps); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := database.MigrateSteps(dbURL, migrationsPath, steps); err != nil {
fmt.Fprintf(os.Stderr, "Error running %d migration steps: %v\n", steps, err)
os.Exit(1)
}
fmt.Printf("Applied %d migration steps successfully\n", steps)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
printUsage()
os.Exit(1)
}
}
func rejectDestructiveProductionMigration(env string, steps int) error {
if env == "production" && steps < 0 {
return fmt.Errorf("destructive schema down is disabled in production; roll back the application image or restore a backup")
}
return nil
}
func printUsage() {
fmt.Println("Usage: migrate <command> [args]")
fmt.Println("")
fmt.Println("Commands:")
fmt.Println(" up Apply all pending migrations")
fmt.Println(" down Rollback all migrations")
fmt.Println(" force <v> Force version to v (fix dirty state)")
fmt.Println(" version Show current migration version")
fmt.Println(" steps <n> Apply n steps (positive=up, negative=down)")
fmt.Println("")
fmt.Println("Environment: Uses database config from Viper (config.yaml)")
}