store,bangerd: add --check-migrations flag for pre-swap schema check
Prerequisite for `banger update`. Before swapping a staged binary
into place, the updater needs to confirm the new bangerd recognises
the running install's DB schema. Without this, an operator could end
up with a service that won't open its store after the binary swap +
restart.
* store.InspectSchemaState(path): opens the DB read-only (reusing
OpenReadOnly's mode=ro DSN), reads the schema_migrations table,
and classifies the relationship between applied and known IDs:
SchemaCompatible (lockstep), SchemaMigrationsNeeded (binary
newer, will auto-migrate on first Open), or SchemaIncompatible
(DB has applied IDs the binary doesn't know about).
Missing schema_migrations table is treated as "all migrations
pending" rather than an error — matches the fresh-install case.
* bangerd --check-migrations: opens the configured DB read-only,
prints a one-line classification, and exits 0/1/2. The exit
code is the contract:
0 — compatible
1 — migrations needed (binary newer; safe to swap)
2 — incompatible (binary older than DB; abort the swap)
Honours --system to pick between system StateDir and user mode.
* bangerdExit indirection so future tests can capture the exit
code without terminating the test process. Production points
at os.Exit.
Tests cover the four classifications: compatible (fully migrated
DB), migrations-needed (only baseline applied), incompatible
(synthetic id=99 inserted), and missing-table (fresh DB). Live
exercise on this dev host returned `migrations needed: pending [3]
(binary will apply on first Open)` and exit 1, matching the
contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c0af3a2de
commit
ec6fc9d185
3 changed files with 327 additions and 0 deletions
|
|
@ -2,18 +2,27 @@ package cli
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"banger/internal/buildinfo"
|
||||
"banger/internal/daemon"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/roothelper"
|
||||
"banger/internal/store"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// bangerdExit is var-injected so tests can capture the exit code
|
||||
// without terminating the test process. Production points at os.Exit.
|
||||
var bangerdExit = os.Exit
|
||||
|
||||
func NewBangerdCommand() *cobra.Command {
|
||||
var systemMode bool
|
||||
var rootHelperMode bool
|
||||
var checkMigrations bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "bangerd",
|
||||
Version: strings.Replace(formatVersionLine(buildinfo.Current()), "banger ", "bangerd ", 1),
|
||||
|
|
@ -25,6 +34,9 @@ func NewBangerdCommand() *cobra.Command {
|
|||
if systemMode && rootHelperMode {
|
||||
return errors.New("choose only one of --system or --root-helper")
|
||||
}
|
||||
if checkMigrations {
|
||||
return runCheckMigrations(cmd, systemMode)
|
||||
}
|
||||
if rootHelperMode {
|
||||
server, err := roothelper.Open()
|
||||
if err != nil {
|
||||
|
|
@ -47,7 +59,70 @@ func NewBangerdCommand() *cobra.Command {
|
|||
}
|
||||
cmd.Flags().BoolVar(&systemMode, "system", false, "run as the owner-user system service")
|
||||
cmd.Flags().BoolVar(&rootHelperMode, "root-helper", false, "run as the privileged root helper service")
|
||||
cmd.Flags().BoolVar(&checkMigrations, "check-migrations", false, "inspect the state DB and report whether this binary's schema matches; exit 0=compatible, 1=migrations needed, 2=incompatible")
|
||||
cmd.SetVersionTemplate("{{.Version}}\n")
|
||||
cmd.CompletionOptions.DisableDefaultCmd = true
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runCheckMigrations is the entry point for `bangerd --check-migrations`.
|
||||
// Used by `banger update` to gate a binary swap on a staged binary
|
||||
// before service restart: if the staged binary doesn't recognise the
|
||||
// running install's schema, the swap is aborted before any host state
|
||||
// changes.
|
||||
//
|
||||
// Exit codes are part of the contract:
|
||||
//
|
||||
// 0 — compatible (no migrations to apply on Open)
|
||||
// 1 — migrations needed (binary newer than DB; safe to swap)
|
||||
// 2 — incompatible (DB has migrations this binary doesn't know;
|
||||
// swapping would leave the daemon unable to open the store)
|
||||
func runCheckMigrations(cmd *cobra.Command, systemMode bool) error {
|
||||
layout := paths.ResolveSystem()
|
||||
if !systemMode {
|
||||
userLayout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layout = userLayout
|
||||
}
|
||||
state, err := store.InspectSchemaState(layout.DBPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect %s: %w", layout.DBPath, err)
|
||||
}
|
||||
out := cmd.OutOrStdout()
|
||||
switch state.Compatibility {
|
||||
case store.SchemaCompatible:
|
||||
fmt.Fprintf(out, "compatible: db at v%d, binary knows up to v%d\n", lastID(state.AppliedIDs), state.KnownMaxID)
|
||||
return nil
|
||||
case store.SchemaMigrationsNeeded:
|
||||
fmt.Fprintf(out, "migrations needed: pending %v (binary will apply on first Open)\n", state.Pending)
|
||||
// Distinct exit code so callers can tell "safe to swap, will
|
||||
// auto-migrate" apart from "compatible, no work pending".
|
||||
// Returning a cobra error would also exit non-zero, but we
|
||||
// want a specific code (1) — and we don't want SilenceErrors
|
||||
// to print our message twice.
|
||||
bangerdExit(1)
|
||||
return nil
|
||||
case store.SchemaIncompatible:
|
||||
fmt.Fprintf(out, "incompatible: db has unknown migrations %v (binary knows up to v%d)\n", state.Unknown, state.KnownMaxID)
|
||||
bangerdExit(2)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected schema-state classification %d", state.Compatibility)
|
||||
}
|
||||
}
|
||||
|
||||
// lastID returns the largest int in xs, or 0 when empty. The schema-
|
||||
// migrations table doesn't guarantee insert order, so we scan rather
|
||||
// than trusting xs[len-1].
|
||||
func lastID(xs []int) int {
|
||||
max := 0
|
||||
for _, x := range xs {
|
||||
if x > max {
|
||||
max = x
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue