From 078e84c681733bd4902e0d48055c1df101f3e723 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 22 Sep 2026 18:08:02 +0200 Subject: [PATCH] Read an adopted or converged node's mode from its state on a re-run of genesis, and refuse a flag that disagrees (hq ADR 0103) --- internal/bootstrap/inuse.go | 31 +++++++++++++++++++++++ internal/bootstrap/inuse_test.go | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/internal/bootstrap/inuse.go b/internal/bootstrap/inuse.go index 58c357b..023d322 100644 --- a/internal/bootstrap/inuse.go +++ b/internal/bootstrap/inuse.go @@ -6,6 +6,7 @@ import ( "net" "strings" + "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/reachable" "github.com/novox/mesh-host/internal/store" ) @@ -76,6 +77,20 @@ func RefuseAMachineInUse(ctx context.Context, o Options, run Runner, say func(st return err } if len(known.Resources) > 0 { + // **The machine says how it was raised** (novox/hq ADR 0103). A re-run must not change + // the node's mode by a flag forgotten or added: without --adopted the bundle would load + // the foundation's dropping filter over the found firewall, and with it on a converged + // machine the filter the node relies on would be removed as no longer carried. + switch adopted := RecordsAdoption(known); { + case adopted && !o.Adopted: + return fmt.Errorf("this machine was raised adopted, and genesis was run again without --adopted. " + + "Run it again the way it was raised: pass --adopted. Returning it to converged is the " + + "controller's act (converge), never genesis's; nothing was changed") + case !adopted && o.Adopted: + return fmt.Errorf("this machine was raised converged, and genesis was run again with --adopted, " + + "which would remove the foundation's filter it relies on. Run it again without --adopted; " + + "returning a node to adopted is the controller's act (adopt); nothing was changed") + } // What genesis raised on an earlier run is the mesh's, and it is what the machine now // serves; the question was answered the first time. say(" in use not asked: this machine carries what an earlier genesis raised") @@ -109,3 +124,19 @@ func RefuseAMachineInUse(ctx context.Context, o Options, run Runner, say func(st "force and every module is taken on it one at a time. Nothing was changed", strings.Join(named, "\n - ")) } + +// RecordsAdoption is whether this machine's state says it is an adopted node: it holds something +// of the mesh's that only an adopted node has — the guard or an opening, under the adoption prefix +// — or something it found and holds. A node the controller converged has neither any more; the +// record of the firewall it found outlives the flip, so it is not read as the mode. +func RecordsAdoption(known store.State) bool { + if len(known.Held) > 0 { + return true + } + for _, r := range known.Resources { + if strings.HasPrefix(r.ID, declaration.AdoptionPrefix) { + return true + } + } + return false +} diff --git a/internal/bootstrap/inuse_test.go b/internal/bootstrap/inuse_test.go index 98ea349..137ceef 100644 --- a/internal/bootstrap/inuse_test.go +++ b/internal/bootstrap/inuse_test.go @@ -120,3 +120,46 @@ func TestAFreshlyInstalledMachineAsMeasuredIsNotInUse(t *testing.T) { t.Errorf("a fresh machine read as in use: containers %v, listeners %v", containers, listeners) } } + +// Defends novox/hq ADR 0103: a machine raised adopted stays adopted if genesis is run again. The +// installer reads the mode from what the machine records, and refuses a flag that disagrees. +func TestARerunWithoutTheFlagOnAnAdoptedMachineIsRefused(t *testing.T) { + o := Options{State: filepath.Join(t.TempDir(), "state.json")} + adoptedState := store.State{Resources: []store.Applied{ + {ID: "store", Type: "container", Target: "mesh-store", Origin: store.OriginCarried}, + {ID: "adoption.guard", Type: "file", Target: "/etc/mesh/guard.nft", Origin: store.OriginCarried}, + }} + if err := store.Save(o.State, adoptedState); err != nil { + t.Fatal(err) + } + m := inUseRunner{ss: inUseSockets} + err := RefuseAMachineInUse(context.Background(), o, m.run, quietly) + if err == nil || !strings.Contains(err.Error(), "pass --adopted") { + t.Fatalf("a re-run without --adopted on an adopted machine was not refused: %v", err) + } + o.Adopted = true + if err := RefuseAMachineInUse(context.Background(), o, m.run, quietly); err != nil { + t.Errorf("a re-run with --adopted on an adopted machine was refused: %v", err) + } +} + +func TestARerunWithTheFlagOnAConvergedMachineIsRefused(t *testing.T) { + o := Options{State: filepath.Join(t.TempDir(), "state.json"), Adopted: true} + converged := store.State{Resources: []store.Applied{ + {ID: "store", Type: "container", Target: "mesh-store", Origin: store.OriginCarried}, + {ID: "base-filter", Type: "file", Target: "/etc/nftables.conf", Origin: store.OriginCarried}, + }, + // Converged by the controller from adopted: the firewall it found is still recorded. + Firewall: &store.FoundFirewall{Kind: "ufw", WasActive: true, DisabledByMesh: true}} + if err := store.Save(o.State, converged); err != nil { + t.Fatal(err) + } + err := RefuseAMachineInUse(context.Background(), o, inUseRunner{ss: inUseSockets}.run, quietly) + if err == nil || !strings.Contains(err.Error(), "without --adopted") { + t.Fatalf("a re-run with --adopted on a converged machine was not refused: %v", err) + } + o.Adopted = false + if err := RefuseAMachineInUse(context.Background(), o, inUseRunner{ss: inUseSockets}.run, quietly); err != nil { + t.Errorf("a converged re-run of a converged machine was refused: %v", err) + } +}