feat: version-aware host-asset preflight for binary-swap deploys (#750)

A binary-swap deploy (make deploy or a manual spx copy) ships a new binary
whose host-config prerequisites — the privileged helper scripts and their
sudoers grant, placed only by setup.sh / setup-ovn.sh — it does not ship.
The node looks healthy and the gap surfaces later as a cryptic launch
failure or a silent security regression.

Add a preflight package that checks the host against what this build
expects, comparing asset content, not existence, so a stale-but-present
asset is caught too. The managed helpers install from quoted heredocs, so
their on-disk bytes equal the heredoc body verbatim; the binary carries
the canonical sha256s and compares.

- spinifex/preflight: CheckHost / CheckHostAt, content hash of the two
  managed helpers and a NOPASSWD grant-line check for the sysctl helper.
- Generated canonicalHashes plus a drift guard that re-extracts the
  heredocs and fails if the committed manifest no longer matches.
- Three surfaces: spx admin preflight, a make deploy gate
  (SKIP_ASSET_PREFLIGHT=1 to bypass), and a non-fatal daemon boot warning.
This commit is contained in:
Josh Siviour
2026-08-10 07:52:39 +10:00
committed by GitHub
parent bc3d59c0c3
commit d178ef5992
8 changed files with 567 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package cmd
import (
"fmt"
"os"
"text/tabwriter"
"github.com/mulgadc/spinifex/spinifex/preflight"
"github.com/spf13/cobra"
)
var adminPreflightQuiet bool
var adminPreflightCmd = &cobra.Command{
Use: "preflight",
Short: "Verify this host's managed assets against what this build expects",
Long: `spx knows its own build (spx version) but not whether this host already
has the privileged helper scripts and sudoers grants that build depends on —
setup.sh and setup-ovn.sh install them, and a binary-swap deploy (make
deploy or a manual binary copy) skips those steps.
Checks compare content, not mere existence, so a present-but-stale asset is
caught too. Exits non-zero if any asset is Missing, Stale, or Ungranted.`,
RunE: runAdminPreflight,
SilenceUsage: true,
}
func init() {
adminCmd.AddCommand(adminPreflightCmd)
adminPreflightCmd.Flags().BoolVar(&adminPreflightQuiet, "quiet", false, "print only non-OK rows")
}
func runAdminPreflight(_ *cobra.Command, _ []string) error {
results := preflight.CheckHost()
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "PATH\tKIND\tSTATUS\tDETAIL")
for _, r := range results {
if adminPreflightQuiet && r.Status == preflight.OK {
continue
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Path, r.Kind, r.Status, r.Detail)
}
w.Flush()
if preflight.HasProblem(results) {
return fmt.Errorf("host-asset preflight found problems — see table above")
}
return nil
}