failopen_lost_bornmarker_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"context"
"go/ast"
"go/parser"
"go/token"
"os"
"reflect"
"testing"
"github.com/pingular/pingularity/internal/config"
"github.com/pingular/pingularity/internal/settings"
"github.com/pingular/pingularity/internal/store"
)
// A birth marker records something only the daemon that created the database
// can know, so the daemon that watched it happen may still complete a stamp a
// store fault lost (see internal/settings) - but only until it exits. A write
// that fails at birth and is not completed before the process restarts leaves
// the install markerless FOREVER: from that boot on the store is established,
// no later controller witnessed anything, and an established store must never be
// stamped (a marker added later claims a birth that cannot be proven). On disk
// that install becomes byte-identical to a genuinely 0.61-or-earlier one, which
// is the shape these tests are about.
//
// This is the test that matters: that shape must still fail CLOSED. Marker
// absence advises and nothing more - warnAmbiguousContainerAccess writes
// nothing, and TestOnlyExplicitInputPersistsAccess pins that no other path in
// main may persist an access decision - so the worst a lost stamp can do is
// cost a recurring warning, never network-reachable access. If a future change
// ever lets marker absence DECIDE something, a dropped write at birth becomes
// that decision taken wrongly and permanently; this test is the tripwire.
// denyWritesTo / allowWritesTo make a REAL store refuse writes while reads keep
// working - the exact shape of the fault (settings load fine, only the stamp
// cannot land). The :memory: pool is one connection, so the per-connection
// pragma sticks across calls.
func denyWritesTo(t *testing.T, st *store.Store) {
t.Helper()
if _, err := st.DB().ExecContext(context.Background(), "PRAGMA query_only = ON"); err != nil {
t.Fatalf("query_only on: %v", err)
}
}
func allowWritesTo(t *testing.T, st *store.Store) {
t.Helper()
if _, err := st.DB().ExecContext(context.Background(), "PRAGMA query_only = OFF"); err != nil {
t.Fatalf("query_only off: %v", err)
}
}
// quietStderr swallows what fn writes to os.Stderr (settings warns there about
// the lost stamp, before any logger exists) so the failure being modelled does
// not look like test output.
func quietStderr(t *testing.T, fn func()) {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "stderr")
if err != nil {
t.Fatalf("temp stderr: %v", err)
}
defer f.Close()
orig := os.Stderr
os.Stderr = f
defer func() { os.Stderr = orig }()
fn()
}
// newLostMarkerContainerStore builds the store a container is left with when its
// birth stamp was LOST to a write error: born under a build that fails closed
// from the first boot (so it was never network-reachable), yet carrying no
// marker, and then established by an ordinary life of configuration and
// history. Every step is real - the write genuinely fails, the marker is
// genuinely absent - so this reproduces the shape rather than asserting it.
//
// What makes the loss PERMANENT is the restart. The controller that watched the
// database come into existence may still complete its own stamp (a later reload
// or settings write finishes it - see the settings package), so this fixture
// drops it while the store is still markerless: from here on, every controller
// is one that never saw the store fresh, and those must refuse forever. That is
// also why the store's later life is written directly rather than through the
// witnessing controller - the daemon that witnessed the birth is gone.
//
// cfg is the boot being modelled, and it seeds the controller exactly as main
// does (establishedContainerController -> testDefaultsFor). Seeding local-only
// here while handing an explicit network scope to reconcileAccess would invent a
// disagreement production never has.
func newLostMarkerContainerStore(t *testing.T, cfg config.Config) (*store.Store, *settings.Controller) {
t.Helper()
ctx := context.Background()
st := openStore(t)
// Birth, with the database refusing writes.
denyWritesTo(t, st)
var born *settings.Controller
var err error
quietStderr(t, func() {
born, err = settings.New(ctx, st, testDefaultsFor(cfg), settings.WithBornVersion("0.62.0-test"))
})
if err != nil {
t.Fatalf("settings.New at birth = %v; a failed stamp must not fail the load", err)
}
if born.BornMarkerErr() == nil {
t.Fatal("fixture precondition: the birth stamp must have failed and been recorded")
}
if _, marked := settingsSnapshot(t, st)[settings.KeyInstallBornVersion]; marked {
t.Fatal("fixture precondition: the store must be markerless")
}
allowWritesTo(t, st) // the hiccup passes; everything else works from here on
// The restart, on a later release: `born` is dropped here without ever
// completing its stamp, and the controller for the boot under test is built
// the one sanctioned way - over a store the ordinary life of configuration
// and history has since made ESTABLISHED.
set := establishedContainerController(t, st, cfg)
if set.BornMarkerErr() != nil {
t.Fatalf("BornMarkerErr = %v on a boot that had nothing to stamp", set.BornMarkerErr())
}
return st, set
}
// THE PROOF. An install whose birth marker was lost to a write error, running in
// a container with no explicit -access/PINGULARITY_ACCESS, must end its boot
// LOCAL-ONLY with nothing written. The marker's absence is a missing signal, not
// permission to guess: guessing open here would put an unauthenticated dashboard
// on the LAN of an install that was born private and never answered the network.
func TestLostBirthMarkerContainerBootStaysLocalOnlyAndWritesNothing(t *testing.T) {
ctx := context.Background()
st, set := newLostMarkerContainerStore(t, config.Config{}) // no explicit access input
before := settingsSnapshot(t, st)
warns := bootAccessDecision(t, config.Config{}, st, set, true) // no explicit access input
if !set.AccessLocalOnly() {
t.Fatal("a LOST birth-marker write opened the dashboard to the network; marker absence must decide nothing")
}
if storedAccessKey(t, st) {
t.Fatal("an access choice was persisted from a missing marker; only explicit operator input may write access")
}
if after := settingsSnapshot(t, st); !reflect.DeepEqual(before, after) {
t.Fatalf("the boot wrote to settings: before=%v after=%v (this path must write nothing at all)", before, after)
}
// Not just in memory: the next boot reads the same fail-closed state.
if err := set.Reload(ctx); err != nil {
t.Fatalf("reload: %v", err)
}
if !set.AccessLocalOnly() {
t.Fatal("access did not survive a reload as local-only")
}
if _, marked := settingsSnapshot(t, st)[settings.KeyInstallBornVersion]; marked {
t.Fatal("the reload stamped the marker on an established store")
}
// The cost of the lost stamp, stated: this install gets the ambiguity
// warning on every boot forever, and the operator's way out is the explicit
// flag. That is the whole damage - a support signal, not an exposure.
if len(warns) != 1 || warns[0].msg != accessAmbiguousWarnMsg {
t.Fatalf("got %d warnings (%v), want exactly the ambiguity warning: a permanently markerless install must at least be explained", len(warns), warns)
}
}
// The surfaced error must also be CONSUMED, or it is dropped again one layer up.
// Settings records WHY the stamp was lost (and warns to stderr, before any
// logger exists); main is what turns it into a log line an operator can still
// find months later, when the only remaining symptom is an ambiguity warning on
// every boot. Source-level, because run()'s boot sequence is not callable from a
// test - the same reason TestOnlyExplicitInputPersistsAccess reads main.go.
func TestBootReportsALostBirthMarker(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "main.go", nil, 0)
if err != nil {
t.Fatalf("parse main.go: %v", err)
}
found := false
ast.Inspect(file, func(n ast.Node) bool {
if sel, ok := n.(*ast.SelectorExpr); ok && sel.Sel.Name == "BornMarkerErr" {
found = true
}
return !found
})
if !found {
t.Error("main.go never reads BornMarkerErr: a birth marker lost to a write error is permanent, and nothing would record why this install is treated as pre-marker for the rest of its life")
}
}
// The way out is the documented one, unchanged by how the store lost its marker:
// an explicit PINGULARITY_ACCESS=network is authoritative at THIS boot, so the
// install is one env var away from being reachable again - and once access is
// settled, the ambiguity warning stops.
//
// What it does NOT do is write an access row, and the fixture is what makes that
// visible: main seeds the controller from the same config the boot carries (def
// := defaultSettings(p.cfg)), so with no stored choice the network scope is
// already the effective value and reconcileAccess finds nothing to reconcile.
// Persisting is the Access tab's Save - which is why the docs tell operators to
// keep the variable rather than assume one run made it stick. An older version
// of this test seeded local-only defaults while passing network here, and
// "proved" a persistence production never performs.
func TestLostBirthMarkerInstallStillRecoversWithExplicitAccess(t *testing.T) {
ctx := context.Background()
cfg := config.Config{Access: "network", AccessExplicit: true}
st, set := newLostMarkerContainerStore(t, cfg)
warns := bootAccessDecision(t, cfg, st, set, true)
if set.AccessLocalOnly() {
t.Fatal("explicit PINGULARITY_ACCESS=network did not open access; that is the documented way out of the lockout")
}
if storedAccessKey(t, st) {
t.Fatal("the boot wrote an access row: with the scope already seeded from the same config there is nothing to reconcile, and claiming otherwise models a state production never reaches")
}
if err := set.Reload(ctx); err != nil {
t.Fatalf("reload: %v", err)
}
// The reload re-overlays the same defaults over a store that still has no
// access row, so the variable still governs - and still nothing is stored.
if set.AccessLocalOnly() {
t.Fatal("the explicit scope did not survive a reload of the same boot's config")
}
if storedAccessKey(t, st) {
t.Fatal("the reload persisted an access choice")
}
if len(warns) != 0 {
t.Fatalf("got %d warnings, want 0: the operator settled access, so there is no ambiguity left to report", len(warns))
}
}