package main import ( "flag" "fmt" "os" "time" "github.com/enmanuel/unibus/pkg/busauth" "github.com/enmanuel/unibus/pkg/membership" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) // runMigrateCLI implements `membershipd migrate-to-kv`, the idempotent move of // the control-plane state from the local SQLite database into replicated // JetStream KV (issue 0003c). It backs up the SQLite file first (VACUUM INTO), // then connects to the target NATS and copies every room/member/key/user into // the KV buckets. Re-running it converges to the same state. // // It runs on the bus host (no auth on the control-plane side), connecting to the // cluster's NATS; --ca pins TLS when the data plane is secured. func runMigrateCLI(args []string) { fs := flag.NewFlagSet("migrate-to-kv", flag.ExitOnError) dbPath := fs.String("db", defaultDBPath, "SQLite database path to migrate FROM") natsURL := fs.String("nats-url", "", "NATS url of the cluster to migrate INTO (required)") ca := fs.String("ca", "", "CA cert to pin TLS on the NATS connection (optional)") replicas := fs.Int("replicas", 1, "KV replication factor (1 for a 1-2 node rollout, 3 for HA quorum)") noBackup := fs.Bool("no-backup", false, "skip the SQLite backup before migrating (NOT recommended)") _ = fs.Parse(args) if *natsURL == "" { fmt.Fprintln(os.Stderr, "membershipd migrate-to-kv: --nats-url is required (the cluster to write the KV buckets into)") os.Exit(2) } // Back up the SQLite database first so a botched migration can be undone. var backupPath string if !*noBackup { bak, err := membership.BackupSQLite(*dbPath) if err != nil { fmt.Fprintf(os.Stderr, "membershipd migrate-to-kv: backup failed: %v\n", err) os.Exit(1) } backupPath = bak fmt.Printf("backed up %s -> %s\n", *dbPath, backupPath) } // Connect to the target NATS (optionally TLS-pinned to the bus CA). natsOpts := []nats.Option{nats.Name("unibus-migrate")} if *ca != "" { tlsCfg, err := busauth.LoadCATLSConfig(*ca) if err != nil { fmt.Fprintf(os.Stderr, "membershipd migrate-to-kv: load CA: %v\n", err) os.Exit(1) } natsOpts = append(natsOpts, nats.Secure(tlsCfg)) } nc, err := nats.Connect(*natsURL, natsOpts...) if err != nil { fmt.Fprintf(os.Stderr, "membershipd migrate-to-kv: connect %q: %v\n", *natsURL, err) os.Exit(1) } defer nc.Close() js, err := jetstream.New(nc) if err != nil { fmt.Fprintf(os.Stderr, "membershipd migrate-to-kv: jetstream: %v\n", err) os.Exit(1) } report, err := membership.MigrateSQLiteToKV(*dbPath, js, membership.JetStreamConfig{ Replicas: *replicas, OpTimeout: 30 * time.Second, }) if err != nil { fmt.Fprintf(os.Stderr, "membershipd migrate-to-kv: %v\n", err) os.Exit(1) } report.BackupPath = backupPath fmt.Printf("migrated to KV (replicas=%d): %d rooms, %d members, %d keys, %d users\n", *replicas, report.Rooms, report.Members, report.Keys, report.Users) if backupPath != "" { fmt.Printf("rollback: restore %s if needed\n", backupPath) } }