README gets a top-level Updating section; docs/privileges.md gains a step-by-step trust-model writeup of `banger update`. The new scripts/publish-banger-release.sh drives the manual release cut: build, tar, sha256sum, cosign sign-blob, verify against the embedded public key, jq-merge into manifest.json, rclone upload to the R2 bucket. Refuses outright if the embedded key is still the placeholder so we can't accidentally publish an unverifiable release. Also folds in gofmt drift accumulated across the updater package and a few sibling files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
4 KiB
Go
127 lines
4 KiB
Go
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),
|
|
Short: "Run the banger daemon",
|
|
SilenceUsage: true,
|
|
SilenceErrors: true,
|
|
Args: noArgsUsage("usage: bangerd"),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
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 {
|
|
return err
|
|
}
|
|
defer server.Close()
|
|
return server.Serve(cmd.Context())
|
|
}
|
|
open := daemon.Open
|
|
if systemMode {
|
|
open = daemon.OpenSystem
|
|
}
|
|
d, err := open(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer d.Close()
|
|
return d.Serve(cmd.Context())
|
|
},
|
|
}
|
|
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
|
|
}
|