package host import ( "context" "errors" "os" "path/filepath" "reflect" "strings" "sync" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/layout" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // resetLayoutGlobals puts the package's published layout state back the // way a fresh process has it, before and after a test that touches it. func resetLayoutGlobals(t *testing.T) { t.Helper() clear := func() { armedLayoutPass.Store(nil) layoutReload.Store(nil) layoutIntentSeen.Store(nil) layoutDefaultGate.Store(nil) layoutDefaultSeen.Store(nil) } clear() t.Cleanup(clear) } func appendIntent(t *testing.T, s release.Store, mode, box string) { t.Helper() if _, err := release.AppendLayoutIntent(context.Background(), s, release.LayoutIntent{Mode: mode, Scope: release.LayoutScope{Box: box}}); err != nil { t.Fatalf("AppendLayoutIntent: %v", err) } } func TestLayoutLegConvergesOnlyWhenTheIntentAppliesToThisBox(t *testing.T) { resetLayoutGlobals(t) s := release.NewDirStore(t.TempDir()) ctx := context.Background() var calls []layout.Mode converge := func(_ context.Context, mode layout.Mode, _ bool) []layout.Result { calls = append(calls, mode) return nil } layoutLegWith(ctx, s, "kass-1", converge) if len(calls) != 0 { t.Fatalf("converged with no intent: %v", calls) } if got := lastLayoutIntent(); got != "" { t.Fatalf("intent seen with none recorded = %q", got) } appendIntent(t, s, release.LayoutModeNested, "kass-2") layoutLegWith(ctx, s, "kass-1", converge) if len(calls) != 0 { t.Fatalf("converged for another box's intent: %v", calls) } appendIntent(t, s, release.LayoutModeFlat, "") layoutLegWith(ctx, s, "kass-1", converge) if !reflect.DeepEqual(calls, []layout.Mode{layout.ModeFlat}) { t.Fatalf("calls = %v, want [flat]", calls) } if got := lastLayoutIntent(); got != release.LayoutModeFlat { t.Fatalf("intent seen = %q, want flat", got) } appendIntent(t, s, release.LayoutModeNested, "kass-1") layoutLegWith(ctx, s, "kass-1", converge) if !reflect.DeepEqual(calls, []layout.Mode{layout.ModeFlat, layout.ModeNested}) { t.Fatalf("calls = %v, want [flat nested]", calls) } if got := lastLayoutIntent(); got != release.LayoutModeNested { t.Fatalf("intent seen = %q, want nested", got) } } // An intent this binary cannot read, or a request whose mode it does not // know, moves nothing, and publishes layout.IntentUnreadable — overwriting // whatever mode was last seen, since a read that failed just now is not // proof the fleet still wants that mode. A later successful read replaces // the marker the same way (TestStartupLayoutIntentPublishesUnreadable). func TestLayoutLegDoesNothingWhenTheIntentIsUnreadable(t *testing.T) { for _, tc := range []struct { name string body string }{ {"newer schema", `{"schema":2,"requests":[{"id":"a","mode":"nested"}]}`}, {"unknown mode", `{"schema":1,"requests":[{"id":"a","mode":"sideways"}]}`}, {"not json", `{`}, } { t.Run(tc.name, func(t *testing.T) { resetLayoutGlobals(t) noteLayoutIntent(release.LayoutModeFlat) s := release.NewDirStore(t.TempDir()) ctx := context.Background() if _, err := s.Put(ctx, release.LayoutIntentKey(), []byte(tc.body), ""); err != nil { t.Fatal(err) } called := false layoutLegWith(ctx, s, "kass-1", func(context.Context, layout.Mode, bool) []layout.Result { called = true return nil }) if called { t.Fatal("converged under an intent it could not read") } if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) { t.Fatalf("intent seen = %q, want unreadable (the last read failed, not the last mode read)", got) } }) } } // With no pass armed (no activator on this box) the leg reads nothing. func TestLayoutLegWithNoPassArmedReadsNothing(t *testing.T) { resetLayoutGlobals(t) runLayoutLeg(context.Background(), refusingStore{t: t}, "kass-1") } // refusingStore fails the test on a read; every other method is the nil // embedded Store's, which panics, which fails it too. type refusingStore struct { release.Store t *testing.T } func (r refusingStore) Get(context.Context, string) ([]byte, string, error) { r.t.Fatal("the store was read") return nil, "", nil } // The leg never runs two passes at once, never makes the serial tick wait // for one, and a move that lands mid-pass costs exactly one more pass // however many moves there were. func TestLayoutLegRunsOneAtATimeAndOnceMoreForAMoveMidPass(t *testing.T) { started := make(chan struct{}, 8) release1 := make(chan struct{}) var runs, inFlight, most atomic.Int32 leg := &layoutLeg{run: func(context.Context) { n := inFlight.Add(1) for { m := most.Load() if n <= m || most.CompareAndSwap(m, n) { break } } if runs.Add(1) == 1 { started <- struct{}{} <-release1 } inFlight.Add(-1) }} ctx := context.Background() returned := make(chan struct{}) go func() { leg.kick(ctx) close(returned) }() select { case <-returned: case <-time.After(5 * time.Second): t.Fatal("kick waited for the pass") } select { case <-started: case <-time.After(5 * time.Second): t.Fatal("the first pass never started") } for i := 0; i < 3; i++ { leg.kick(ctx) } close(release1) leg.wait() if got := runs.Load(); got != 2 { t.Fatalf("passes = %d, want 2 (the first, and one for the moves mid-pass)", got) } if got := most.Load(); got != 1 { t.Fatalf("%d passes ran at once", got) } // Idle again: the next move runs one pass. leg.kick(ctx) leg.wait() if got := runs.Load(); got != 3 { t.Fatalf("passes after an idle kick = %d, want 3", got) } } // A kick that lands between the runner's last look at the dirty flag and // its going idle is taken back up by that runner, not lost. func TestLayoutLegTakesUpAKickThatLandsAsItGoesIdle(t *testing.T) { leg := &layoutLeg{run: func(context.Context) { t.Fatal("kick started a second runner") }} leg.running.Store(true) // a runner past its last look at dirty leg.kick(context.Background()) if !leg.goIdle() { t.Fatal("the runner went idle over a pending kick") } if !leg.running.Load() { t.Fatal("the runner going again is not marked running") } leg.dirty.Store(false) if leg.goIdle() { t.Fatal("the runner went again with nothing pending") } if leg.running.Load() { t.Fatal("an idle leg is marked running") } } // Kicks from several goroutines at once, many times over: every one is // answered and the leg ends idle with nothing pending (run under -race). func TestLayoutLegNeverLosesAKick(t *testing.T) { for i := 0; i < 200; i++ { var runs atomic.Int32 leg := &layoutLeg{run: func(context.Context) { runs.Add(1) }} var wg sync.WaitGroup for k := 0; k < 4; k++ { wg.Add(1) go func() { defer wg.Done() leg.kick(context.Background()) }() } wg.Wait() leg.wait() if runs.Load() == 0 { t.Fatal("four kicks ran no pass") } if leg.dirty.Load() { t.Fatalf("iteration %d: a kick was left pending with the leg idle", i) } } } // Recovery runs after the startup unit sweep and before the activator // starts, and it runs even when the sweep failed, told so. func TestStartActivatorRecoversAfterTheSweepAndBeforeStart(t *testing.T) { for _, tc := range []struct { name string sweepErr error swept bool }{ {"sweep ok", nil, true}, {"sweep failed", errors.New("channel down"), false}, } { t.Run(tc.name, func(t *testing.T) { var events []string var gotSwept bool startActivator(context.Background(), func(context.Context) error { events = append(events, "sweep"); return tc.sweepErr }, func(_ context.Context, swept bool) { events = append(events, "recover"); gotSwept = swept }, func() { events = append(events, "start") }) if want := []string{"sweep", "recover", "start"}; !reflect.DeepEqual(events, want) { t.Fatalf("order = %v, want %v", events, want) } if gotSwept != tc.swept { t.Fatalf("swept = %v, want %v", gotSwept, tc.swept) } }) } } // The intent recovery runs under at start: "" when none applies, the // recorded mode when one does, and IntentUnreadable whenever the box // cannot be sure, which holds a completed revert fenced. func TestStartupLayoutIntent(t *testing.T) { ctx := context.Background() resetLayoutGlobals(t) // Recorded intents are what this test is about; opt out of the nested // default so "no object" reads as none (TestStartupAndMintReadTheDefault // covers the default). t.Setenv(layoutDefaultEnv, "flat") if got := startupLayoutIntent(ctx, nil, errors.New("no bucket"), "kass-1"); got != layout.IntentUnreadable { t.Fatalf("no store: %q", got) } if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) { t.Fatalf("intent seen after no store = %q, want unreadable", got) } s := release.NewDirStore(t.TempDir()) if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != "" { t.Fatalf("no object: %q", got) } if got := lastLayoutIntent(); got != "" { t.Fatalf("intent seen after no object = %q, want none (a successful read replaces the marker)", got) } appendIntent(t, s, release.LayoutModeNested, "kass-2") if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != "" { t.Fatalf("another box's intent: %q", got) } if got := lastLayoutIntent(); got != "" { t.Fatalf("intent seen after another box's intent = %q, want none", got) } appendIntent(t, s, release.LayoutModeNested, "") if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != layout.ModeNested { t.Fatalf("fleet intent: %q", got) } if got := lastLayoutIntent(); got != release.LayoutModeNested { t.Fatalf("intent seen = %q", got) } for _, body := range []string{ `{"schema":2,"requests":[]}`, `{"schema":1,"requests":[{"id":"a","mode":"sideways"}]}`, } { bad := release.NewDirStore(t.TempDir()) if _, err := bad.Put(ctx, release.LayoutIntentKey(), []byte(body), ""); err != nil { t.Fatal(err) } if got := startupLayoutIntent(ctx, bad, nil, "kass-1"); got != layout.IntentUnreadable { t.Fatalf("%s: %q", body, got) } // The prior call published "nested"; this failed read must overwrite // it with the marker, not leave the stale mode standing. if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) { t.Fatalf("%s: intent seen = %q, want unreadable to overwrite the last known mode", body, got) } noteLayoutIntent(release.LayoutModeNested) // restore, for the next iteration's overwrite check } } // Startup reads the intent only when a journal is there to recover. With // none, recovery has nothing to do, so it runs under "" without touching // the deploy store, and the published intent is left for the first serial // leg to set. func TestStartupRecoveryIntentReadsOnlyWithAJournal(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) appendIntent(t, s, release.LayoutModeNested, "") dd := t.TempDir() opened := 0 open := func(context.Context) (release.Store, error) { opened++; return s, nil } resetLayoutGlobals(t) if got := startupRecoveryIntent(ctx, dd, open, "kass-1"); got != "" || opened != 0 { t.Fatalf("no layout directory: intent %q, store opened %d times", got, opened) } dir := filepath.Join(dd, "layout") if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, ".h.example.com.tmp-1"), nil, 0o600); err != nil { t.Fatal(err) } if got := startupRecoveryIntent(ctx, dd, open, "kass-1"); got != "" || opened != 0 { t.Fatalf("a temp file only: intent %q, store opened %d times", got, opened) } if m := layoutIntentSeen.Load(); m != nil { t.Fatalf("published intent %q with no journal; the first serial leg sets it", *m) } if err := os.WriteFile(provision.LayoutJournalPath(dd, "h.example.com"), []byte("{}"), 0o600); err != nil { t.Fatal(err) } if got := startupRecoveryIntent(ctx, dd, open, "kass-1"); got != layout.ModeNested || opened != 1 { t.Fatalf("with a journal: intent %q, store opened %d times", got, opened) } if got := lastLayoutIntent(); got != release.LayoutModeNested { t.Fatalf("intent seen = %q", got) } } // TestStartupLayoutIntentPublishesUnreadable pins the fix on its own: a box // whose every read fails at startup must report layout.IntentUnreadable, not // "" (none) — recovery already holds a completed revert fenced under exactly // that intent, and the published status must not contradict it. func TestStartupLayoutIntentPublishesUnreadable(t *testing.T) { resetLayoutGlobals(t) ctx := context.Background() if got := lastLayoutIntent(); got != "" { t.Fatalf("intent seen before any read = %q, want none", got) } if got := startupLayoutIntent(ctx, nil, errors.New("no deploy store configured"), "kass-1"); got != layout.IntentUnreadable { t.Fatalf("startupLayoutIntent = %q, want unreadable", got) } if got := lastLayoutIntent(); got != string(layout.IntentUnreadable) { t.Fatalf("intent seen = %q, want unreadable, not none", got) } } // The layout leg rides the serial move after the schedule leg, and like // every other leg it does not run on the boot seed or an unchanged serial. func TestFastTickRunsTheLayoutLegAfterTheScheduleLeg(t *testing.T) { env := newFastFixture(t) var events []string d := env.deps(func() { events = append(events, "pass") }) d.scheduleLeg = func(context.Context) { events = append(events, "schedule") } d.layoutLeg = func(context.Context) { events = append(events, "layout") } now := time.Unix(1754640000, 0) ctx := context.Background() if err := release.BumpSerial(ctx, env.store, now); err != nil { t.Fatal(err) } fastTick(ctx, d, now) fastTick(ctx, d, now.Add(10*time.Second)) if len(events) != 0 { t.Fatalf("seed or unchanged serial ran %v", events) } if err := release.BumpSerial(ctx, env.store, now); err != nil { t.Fatal(err) } fastTick(ctx, d, now.Add(20*time.Second)) if want := []string{"pass", "schedule", "layout"}; !reflect.DeepEqual(events, want) { t.Fatalf("legs = %v, want %v", events, want) } } // The pass the agent builds: moves serialize with config sync, the probe // gate is the one handed in, routing reloads through the published edge // (a no-op before the edge is up), awake hosts are deferred, and with no // activator the fence is a nil interface rather than a nil pointer in one. func TestNewLayoutPassWiring(t *testing.T) { resetLayoutGlobals(t) reg := restartTestRegistry(t) dd := t.TempDir() var gateCalls int gate := func() error { gateCalls++; return nil } p := newLayoutPass(reg, nil, dd, gate, true) if p.Fence != nil { t.Fatalf("Fence = %#v with no activator, want a nil interface", p.Fence) } if !p.SweptAtStart || !p.DeferAwake || p.Registry != reg || p.DataDir != dd || p.Logf == nil { t.Fatalf("pass = %+v", p) } if err := p.NestedOK(); err != nil || gateCalls != 1 { t.Fatalf("NestedOK is not the gate handed in: err %v, calls %d", err, gateCalls) } held := false p.Serialize(func() { held = !configSyncMu.TryLock() }) if !held { t.Fatal("Serialize ran without the config-sync lock") } if !configSyncMu.TryLock() { t.Fatal("Serialize left the config-sync lock held") } configSyncMu.Unlock() p.Reload() // nothing published yet: must not panic reloads := 0 f := func() { reloads++ } layoutReload.Store(&f) p.Reload() if reloads != 1 { t.Fatalf("reloads = %d, want 1", reloads) } if q := newLayoutPass(reg, nil, dd, gate, false); q.SweptAtStart { t.Fatal("SweptAtStart set after a failed sweep") } } // The one production SyncExec caller passes the journal predicate: a host // with a layout journal gets no bundle, and its sibling does. func TestSyncExecSerializedSkipsAHostWithALayoutJournal(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) reg := restartTestRegistry(t) dd := t.TempDir() privText, _, err := appconfig.GenerateKey() if err != nil { t.Fatal(err) } priv, err := appconfig.ParsePrivateKey(privText, "test") if err != nil { t.Fatal(err) } if err := appconfig.SetEnv(ctx, s, "bab", "eleven", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatal(err) } held, sibling := "held.example.com", "sib.example.com" for _, h := range []string{held, sibling} { if err := reg.Upsert(registry.Route{Host: h, Kind: "instance", App: "eleven", Account: "bab", Status: "ready", Socket: "/run/carlos/" + h + ".sock"}); err != nil { t.Fatal(err) } } journal := provision.LayoutJournalPath(dd, held) if err := os.MkdirAll(filepath.Dir(journal), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(journal, []byte("{}"), 0o600); err != nil { t.Fatal(err) } if err := syncExecSerialized(ctx, reg, s, priv, dd, nil); err != nil { t.Fatal(err) } if _, err := os.Lstat(filepath.Join(dd, held+".config.env")); !os.IsNotExist(err) { t.Fatalf("a held host got a bundle (err %v)", err) } if b, err := os.ReadFile(filepath.Join(dd, sibling+".config.env")); err != nil || string(b) != "LOG_LEVEL=debug\n" { t.Fatalf("sibling bundle = %q, %v", b, err) } } // TestLayoutStatusReportWithNoPassArmedIsNil pins the snapshot writer's // omission rule (R15): with no activator, and so no layout pass, there is // nothing to move and nothing to report — the status snapshot must omit the // block entirely rather than guess at a mode. // With no pass armed (no activator on the box) the report is not nil but // "unmanaged": rows judged by path shape, so the console's downgrade guard // can still tell a nested database from a flat one, and a box that never // arms an activator does not block its fleet's rollback by silence. func TestLayoutStatusReportWithNoPassArmedIsUnmanagedByPathShape(t *testing.T) { resetLayoutGlobals(t) t.Setenv("CARLOS_DATA_DIR", "/data") got := layoutStatusReport([]registry.Route{ {Host: "nested.example.com", Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath("/data", "nested.example.com"), Socket: provision.NestedSocket("nested.example.com")}, // Unflagged but under the instance tree: still nested by shape. {Host: "unflagged.example.com", Kind: "instance", DBPath: provision.NestedDBPath("/data", "unflagged.example.com"), Socket: provision.NestedSocket("unflagged.example.com")}, {Host: "flat.example.com", Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath("/data", "flat.example.com"), Socket: provision.DefaultSocket("flat.example.com")}, {Host: "external.example.com", Kind: "instance", Socket: provision.DefaultSocket("external.example.com")}, {Host: "side.example.com", Kind: "sidecar"}, {Host: "static.example.com", Kind: "static"}, }) if got == nil || got.Mode != LayoutModeUnmanaged { t.Fatalf("report = %+v, want Mode unmanaged with no pass armed", got) } want := map[string]string{"nested.example.com": "nested", "unflagged.example.com": "nested", "flat.example.com": "flat", "external.example.com": "external"} if len(got.Rows) != len(want) { t.Fatalf("rows = %+v, want %v", got.Rows, want) } for host, layout := range want { if got.Rows[host].Layout != layout { t.Errorf("%s = %+v, want %s", host, got.Rows[host], layout) } } } // A host mid-move (journal) or held after a refused move (hold marker) is // fenced on an unmanaged box too, whatever its row's paths say: the // files may be between two directories, and the console's downgrade guard // must not read the flat-pathed row as flat. func TestUnmanagedLayoutReportFencesAJournalledOrHeldHost(t *testing.T) { resetLayoutGlobals(t) dd := t.TempDir() t.Setenv("CARLOS_DATA_DIR", dd) if err := os.MkdirAll(filepath.Join(dd, "layout"), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(provision.LayoutJournalPath(dd, "moving.example.com"), []byte("{}"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dd, "layout", "held.example.com.hold"), []byte("{}"), 0o600); err != nil { t.Fatal(err) } flat := func(host string) registry.Route { return registry.Route{Host: host, Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath(dd, host), Socket: provision.DefaultSocket(host)} } got := layoutStatusReport([]registry.Route{flat("moving.example.com"), flat("held.example.com"), flat("flat.example.com")}) want := map[string]string{"moving.example.com": "fenced", "held.example.com": "fenced", "flat.example.com": "flat"} if len(got.Rows) != len(want) { t.Fatalf("rows = %+v, want %v", got.Rows, want) } for host, layout := range want { if got.Rows[host].Layout != layout { t.Errorf("%s = %+v, want %s", host, got.Rows[host], layout) } } } // mintLayoutIntent (ReconcileConfig.Layout for this box): an absent intent // object is "" with no error, so mints proceed flat; only a real read // failure, or a request this binary cannot read, is an error that defers // them. func TestMintLayoutIntentAbsentIsFlatAndUnreadableIsAnError(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if mode, err := mintLayoutIntent(ctx, s); err != nil || mode != "" { t.Fatalf("absent object = %q %v, want \"\" and no error", mode, err) } if _, err := s.Put(ctx, release.LayoutIntentKey(), []byte(`{"schema":2,"requests":[]}`), ""); err != nil { t.Fatal(err) } if _, err := mintLayoutIntent(ctx, s); err == nil { t.Fatal("a newer schema must be an error, so the reconciler defers rather than mints flat") } } // TestLayoutStatusReportModeFollowsTheLastIntentSeen pins the three-way // Mode mapping the amendment (R15) puts on the report, distinct from the // brief's original two-word doc: "none" before any intent is recorded, the // mode itself once one is, and "unreadable" when the last read of it // failed — never silently downgraded to "none", which would read as "this // box wants flat" when the box in fact could not tell. func TestLayoutStatusReportModeFollowsTheLastIntentSeen(t *testing.T) { resetLayoutGlobals(t) dd, sd := t.TempDir(), t.TempDir() armedLayoutPass.Store(&layout.Pass{DataDir: dd, SocketDir: sd}) if got := layoutStatusReport(nil); got == nil || got.Mode != "none" { t.Fatalf("mode with no intent seen = %+v, want none", got) } noteLayoutIntent(release.LayoutModeFlat) if got := layoutStatusReport(nil); got == nil || got.Mode != "flat" { t.Fatalf("mode after a flat intent = %+v, want flat", got) } noteLayoutIntent(release.LayoutModeNested) if got := layoutStatusReport(nil); got == nil || got.Mode != "nested" { t.Fatalf("mode after a nested intent = %+v, want nested", got) } noteLayoutIntent(string(layout.IntentUnreadable)) if got := layoutStatusReport(nil); got == nil || got.Mode != "unreadable" { t.Fatalf("mode after an unreadable read = %+v, want unreadable", got) } } // TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes pins R15's row // selection: an ActivatorManaged row goes through the pass's own Classify // (proven here by a row shaped to classify as foreign, so the reason comes // through too), an unflagged socket instance route reports "external", and // everything else — a sidecar, a static route, a service route, and an // instance route with no socket at all — carries no row, because the // report is not the place any of those are decided. func TestLayoutStatusReportRowsCoverOnlyInstanceKindRoutes(t *testing.T) { resetLayoutGlobals(t) dd, sd := t.TempDir(), t.TempDir() armedLayoutPass.Store(&layout.Pass{DataDir: dd, SocketDir: sd}) noteLayoutIntent(release.LayoutModeNested) routes := []registry.Route{ // ActivatorManaged, but its paths match neither default — Classify // must be the thing deciding this, not a guess made here. {Host: "managed.example.com", Kind: "instance", Hibernate: true, Socket: "/elsewhere/managed.sock", DBPath: "/elsewhere/managed.db", Status: "ready"}, // Hibernate=false with a socket: externally supervised. {Host: "external.example.com", Kind: "instance", Hibernate: false, Socket: "/elsewhere/external.sock"}, // A sidecar, explicitly out of scope (R15, finding S6). {Host: "side.example.com", Kind: "sidecar"}, // Not instance-kind at all. {Host: "static.example.com", Kind: "static"}, {Host: "svc.example.com", Kind: "service"}, // Instance-kind but no socket and not hibernate-flagged — neither // managed nor the "unflagged socket" shape. {Host: "nosock.example.com", Kind: "instance"}, } report := layoutStatusReport(routes) if report == nil { t.Fatal("report = nil, want one with a pass armed") } if report.Mode != "nested" { t.Errorf("mode = %q, want nested", report.Mode) } if len(report.Rows) != 2 { t.Fatalf("rows = %+v, want exactly managed and external", report.Rows) } managed := report.Rows["managed.example.com"] if managed.Layout != "foreign" || managed.Reason == "" { t.Errorf("managed row = %+v, want foreign with a reason from Classify", managed) } external := report.Rows["external.example.com"] if external.Layout != "external" || external.Reason != "" { t.Errorf("external row = %+v, want external with no reason", external) } for _, host := range []string{"side.example.com", "static.example.com", "svc.example.com", "nosock.example.com"} { if _, ok := report.Rows[host]; ok { t.Errorf("%s has a row, want none", host) } } } // Landing follow-up 3: a held host makes the leg come back on its own, so // an operator who clears the reason on a quiet fleet waits awakeRetry and // not the next serial move; an all-quiet result does not. func TestLogLayoutResultsRetriesForHeldAndAwake(t *testing.T) { if logLayoutResults([]layout.Result{{Host: "h", Outcome: "migrated"}, {Host: "i", Outcome: "unchanged"}}) { t.Fatal("retry asked with nothing pending") } if !logLayoutResults([]layout.Result{{Host: "h", Outcome: "held", Reason: "x has 2 names"}}) { t.Fatal("no retry for a held host") } if !logLayoutResults([]layout.Result{{Host: "h", Outcome: "deferred", Reason: "awake (stopped for the move in 10m0s unless it sleeps first)"}}) { t.Fatal("no retry for an awake host") } } // A hold marker, like a journal, is something startup acts on, so it too // makes the intent worth reading before the activator starts. func TestStartupRecoveryIntentReadsWithAHoldMarker(t *testing.T) { resetLayoutGlobals(t) dd := t.TempDir() opened := false open := func(context.Context) (release.Store, error) { opened = true s := release.NewDirStore(t.TempDir()) appendIntent(t, s, release.LayoutModeNested, "") return s, nil } if err := os.MkdirAll(filepath.Join(dd, "layout"), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dd, "layout", "h.example.com.hold"), []byte(`{"host":"h.example.com","reason":"x"}`), 0o600); err != nil { t.Fatal(err) } if got := startupRecoveryIntent(context.Background(), dd, open, "kass-1"); got != layout.ModeNested || !opened { t.Fatalf("intent = %q opened=%v, want nested read for a hold marker", got, opened) } } func TestFastTickLayoutPrecedesClaimSerialNotify(t *testing.T) { t.Parallel() for _, occupied := range []bool{false, true} { t.Run(map[bool]string{false: "empty", true: "full"}[occupied], func(t *testing.T) { env := newFastFixture(t) notify := make(chan struct{}, 1) if occupied { notify <- struct{}{} } wantBefore := len(notify) d := env.deps(func() {}) d.serialNotify = notify layoutCalls := 0 d.layoutLeg = func(context.Context) { layoutCalls++ if len(notify) != wantBefore { t.Error("claim notification preceded layout leg") } } now := time.Unix(1754640000, 0) if err := release.BumpSerial(t.Context(), env.store, now); err != nil { t.Fatal(err) } fastTick(t.Context(), d, now) fastTick(t.Context(), d, now.Add(10*time.Second)) if layoutCalls != 0 || len(notify) != wantBefore { t.Fatal("seed or unchanged serial ran layout/claim notification") } if err := release.BumpSerial(t.Context(), env.store, now); err != nil { t.Fatal(err) } done := make(chan struct{}) go func() { fastTick(t.Context(), d, now.Add(20*time.Second)) close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("serial notification blocked fast tick") } if layoutCalls != 1 || len(notify) != 1 { t.Fatalf("layout calls=%d notifications=%d", layoutCalls, len(notify)) } }) } } func TestSyncExecSerializedFencedClaimHost(t *testing.T) { t.Parallel() reg := restartTestRegistry(t) store := release.NewDirStore(t.TempDir()) dd := t.TempDir() host := "held.example.com" if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "bab", App: "app", Status: "ready", Socket: "/unused.sock"}); err != nil { t.Fatal(err) } journal := provision.LayoutJournalPath(dd, host) if err := os.MkdirAll(filepath.Dir(journal), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(journal, []byte("{}"), 0600); err != nil { t.Fatal(err) } writes := hostwrite.Config{Enabled: true} if err := syncExecSerialized(t.Context(), reg, store, nil, dd, nil, writes); err != nil { t.Fatalf("fenced host required a claim provider: %v", err) } if err := os.Remove(journal); err != nil { t.Fatal(err) } if err := syncExecSerialized(t.Context(), reg, store, nil, dd, nil, writes); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("unfenced host lost its host-write option: %v", err) } } // armDefaultGate makes the default's readiness answer what the test says. func armDefaultGate(t *testing.T, err error) { t.Helper() gate := func() error { return err } layoutDefaultGate.Store(&gate) } // defaultLayoutIntent (design §8.6, "Nested by default") is nested only // when every readiness rule holds; each rule that fails names itself. func TestDefaultLayoutIntentRules(t *testing.T) { resetLayoutGlobals(t) prev := layoutBuild t.Cleanup(func() { layoutBuild = prev }) layoutBuild = func() string { return "v0.25.0" } // No activator armed: not ready, says so. t.Setenv(layoutDefaultEnv, "") if mode, reason := defaultLayoutIntent(); mode != "" || !strings.Contains(reason, "no activator") { t.Fatalf("no gate = %q %q", mode, reason) } // Gate refusing: not ready, with the gate's reason. armDefaultGate(t, errors.New("instance control socket not listening")) if mode, reason := defaultLayoutIntent(); mode != "" || !strings.Contains(reason, "isolation not ready: instance control socket not listening") { t.Fatalf("gate refusing = %q %q", mode, reason) } // Ready: nested by default. armDefaultGate(t, nil) if mode, reason := defaultLayoutIntent(); mode != release.LayoutModeNested || reason != "" { t.Fatalf("ready = %q %q, want nested", mode, reason) } // The deployment opt-out wins over readiness. t.Setenv(layoutDefaultEnv, "flat") if mode, reason := defaultLayoutIntent(); mode != "" || reason != "" { t.Fatalf("CARLOS_LAYOUT_DEFAULT=flat = %q %q, want none with no reason", mode, reason) } t.Setenv(layoutDefaultEnv, " Nested ") if mode, _ := defaultLayoutIntent(); mode != release.LayoutModeNested { t.Fatalf("CARLOS_LAYOUT_DEFAULT=' Nested ' = %q, want nested (folded)", mode) } // A typo stays flat and says so. t.Setenv(layoutDefaultEnv, "nestd") if mode, reason := defaultLayoutIntent(); mode != "" || !strings.Contains(reason, "CARLOS_LAYOUT_DEFAULT=nestd is not nested or flat") { t.Fatalf("typo = %q %q", mode, reason) } // A release below the floor is not ready, however the gate answers; // an unplaceable build (this code, running) passes the floor. t.Setenv(layoutDefaultEnv, "nested") layoutBuild = func() string { return "v0.24.2" } if mode, reason := defaultLayoutIntent(); mode != "" || !strings.Contains(reason, "runs v0.24.2, below v0.25.0") { t.Fatalf("below floor = %q %q", mode, reason) } layoutBuild = func() string { return "abc1234" } if mode, _ := defaultLayoutIntent(); mode != release.LayoutModeNested { t.Fatalf("dev build = %q, want nested", mode) } } // The serial leg under no recorded intent: a ready box converges nested, // never forced, and status says nested by default; a box that is not ready // converges nothing and status says why; a recorded flat intent is the // opt-out and wins; a recorded nested intent is not "by default". func TestLayoutLegUsesTheDefaultWhenNoIntentIsRecorded(t *testing.T) { resetLayoutGlobals(t) armedLayoutPass.Store(&layout.Pass{DataDir: t.TempDir(), SocketDir: t.TempDir()}) t.Setenv(layoutDefaultEnv, "") prev := layoutBuild t.Cleanup(func() { layoutBuild = prev }) layoutBuild = func() string { return "v0.25.0" } s := release.NewDirStore(t.TempDir()) ctx := context.Background() type call struct { mode layout.Mode force bool } var calls []call converge := func(_ context.Context, mode layout.Mode, force bool) []layout.Result { calls = append(calls, call{mode, force}) return nil } armDefaultGate(t, errors.New("probe: pid1 fail")) layoutLegWith(ctx, s, "kass-1", converge) if len(calls) != 0 { t.Fatalf("converged while not ready: %v", calls) } if got := layoutStatusReport(nil); got.Mode != "none" || got.Default || !strings.Contains(got.DefaultBlocked, "isolation not ready: probe: pid1 fail") { t.Fatalf("report while not ready = %+v", got) } armDefaultGate(t, nil) layoutLegWith(ctx, s, "kass-1", converge) if !reflect.DeepEqual(calls, []call{{layout.ModeNested, false}}) { t.Fatalf("calls = %v, want one unforced nested converge", calls) } if got := layoutStatusReport(nil); got.Mode != "nested" || !got.Default || got.DefaultBlocked != "" { t.Fatalf("report when nested by default = %+v", got) } // The opt-out: a recorded flat intent wins over a ready box. appendIntent(t, s, release.LayoutModeFlat, "") layoutLegWith(ctx, s, "kass-1", converge) if len(calls) != 2 || calls[1].mode != layout.ModeFlat { t.Fatalf("calls after a flat intent = %v, want a flat converge", calls) } if got := layoutStatusReport(nil); got.Mode != "flat" || got.Default || got.DefaultBlocked != "" { t.Fatalf("report under a recorded flat intent = %+v", got) } // A recorded nested intent is nested on purpose, not by default. appendIntent(t, s, release.LayoutModeNested, "kass-1") layoutLegWith(ctx, s, "kass-1", converge) if got := layoutStatusReport(nil); got.Mode != "nested" || got.Default { t.Fatalf("report under a recorded nested intent = %+v, want nested and not default", got) } } // Startup recovery and the reconciler's mint read the same default: with // no intent and a ready box both answer nested; with no intent and a box // that is not ready both answer none; CARLOS_LAYOUT_DEFAULT=flat answers // none on a ready box. func TestStartupAndMintReadTheDefault(t *testing.T) { resetLayoutGlobals(t) t.Setenv(layoutDefaultEnv, "") prev := layoutBuild t.Cleanup(func() { layoutBuild = prev }) layoutBuild = func() string { return "v0.25.0" } ctx := context.Background() s := release.NewDirStore(t.TempDir()) // Startup never asks the gate: under the nested default it runs // recovery fail-closed (IntentUnreadable keeps holds and journals), // whatever the gate would say, and status says why. armDefaultGate(t, errors.New("not yet run")) if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != layout.IntentUnreadable { t.Fatalf("startup, gate not ready = %q, want unreadable (holds kept)", got) } if d := lastLayoutDefault(); d.Applied || !strings.Contains(d.Blocked, "agent starting: holds and journals kept") { t.Fatalf("default state at start = %+v", d) } armDefaultGate(t, nil) if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != layout.IntentUnreadable { t.Fatalf("startup, gate ready = %q, want unreadable: startup does not trust the gate", got) } // The mint asks the gate; not ready is flat... armDefaultGate(t, errors.New("not yet run")) if mode, err := mintLayoutIntent(ctx, s); err != nil || mode != "" { t.Fatalf("mint, not ready = %q %v, want none", mode, err) } // ...unless the box already holds an activator-managed nested row, // which is proof it has moved: a fresh row beside it is minted nested // before the gate's first answer. A nested row that is not managed, // or flat rows, are not that proof. t.Setenv("CARLOS_DATA_DIR", "/data") nestedManaged := registry.Route{Host: "a.example.com", Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath("/data", "a.example.com"), Socket: provision.NestedSocket("a.example.com")} nestedUnflagged := registry.Route{Host: "u.example.com", Kind: "instance", DBPath: provision.NestedDBPath("/data", "u.example.com"), Socket: provision.NestedSocket("u.example.com")} flat := registry.Route{Host: "f.example.com", Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath("/data", "f.example.com"), Socket: provision.DefaultSocket("f.example.com")} rows := func(rs ...registry.Route) func() ([]registry.Route, error) { return func() ([]registry.Route, error) { return rs, nil } } if mode, err := mintLayoutIntentWith(ctx, s, rows(flat, nestedUnflagged)); err != nil || mode != "" { t.Fatalf("mint, not ready, no managed nested row = %q %v, want none", mode, err) } if mode, err := mintLayoutIntentWith(ctx, s, rows(flat, nestedManaged)); err != nil || mode != release.LayoutModeNested { t.Fatalf("mint, not ready, a managed nested row = %q %v, want nested", mode, err) } if _, err := mintLayoutIntentWith(ctx, s, func() ([]registry.Route, error) { return nil, errors.New("registry locked") }); err == nil { t.Fatal("a registry that cannot be listed must defer the mint, not guess") } armDefaultGate(t, nil) if mode, err := mintLayoutIntent(ctx, s); err != nil || mode != release.LayoutModeNested { t.Fatalf("mint, ready = %q %v, want nested", mode, err) } // The opt-out: startup drops holds as a recorded flat would, and the // mint is flat even beside a nested row. t.Setenv(layoutDefaultEnv, "flat") if got := startupLayoutIntent(ctx, s, nil, "kass-1"); got != "" { t.Fatalf("startup, opt-out = %q, want none", got) } if mode, err := mintLayoutIntentWith(ctx, s, rows(nestedManaged)); err != nil || mode != "" { t.Fatalf("mint, opt-out = %q %v, want none", mode, err) } // A recorded intent still wins over the opt-out. appendIntent(t, s, release.LayoutModeNested, "") if mode, err := mintLayoutIntent(ctx, s); err != nil || mode != release.LayoutModeNested { t.Fatalf("mint under a recorded nested intent with the opt-out set = %q %v, want nested", mode, err) } } // newLayoutPass arms the default's gate with the same nestedOK the pass // refuses nested work with. func TestNewLayoutPassArmsTheDefaultGate(t *testing.T) { resetLayoutGlobals(t) want := errors.New("the pass's gate") newLayoutPass(nil, nil, t.TempDir(), func() error { return want }, true) gate := layoutDefaultGate.Load() if gate == nil || !errors.Is((*gate)(), want) { t.Fatal("the default's gate is not the pass's nestedOK") } } // newLayoutLeg kicks the leg once at build when a pass is armed, so the // first evaluation with the gate's real answer never waits on a console // bump; with no pass armed there is nothing to evaluate and no kick. func TestNewLayoutLegKicksOnceWhenAPassIsArmed(t *testing.T) { resetLayoutGlobals(t) var runs atomic.Int32 run := func(context.Context) bool { runs.Add(1); return false } leg := newLayoutLeg(context.Background(), run) leg.wait() if runs.Load() != 0 { t.Fatalf("runs with no pass armed = %d, want 0", runs.Load()) } armedLayoutPass.Store(&layout.Pass{DataDir: t.TempDir(), SocketDir: t.TempDir()}) leg = newLayoutLeg(context.Background(), run) leg.wait() if runs.Load() != 1 { t.Fatalf("runs with a pass armed = %d, want exactly one startup kick", runs.Load()) } // Later kicks are the serial loop's, as before. leg.kick(context.Background()) leg.wait() if runs.Load() != 2 { t.Fatalf("runs after a serial kick = %d, want 2", runs.Load()) } } // A run under the nested default that the gate refuses asks for the leg's // retry, so a quiet fleet re-asks the gate on its own; the opt-out and a // typo do not, because nothing they wait on will change. func TestLayoutLegRetriesWhileTheDefaultIsBlocked(t *testing.T) { resetLayoutGlobals(t) armedLayoutPass.Store(&layout.Pass{DataDir: t.TempDir(), SocketDir: t.TempDir()}) prev := layoutBuild t.Cleanup(func() { layoutBuild = prev }) layoutBuild = func() string { return "v0.25.0" } s := release.NewDirStore(t.TempDir()) ctx := context.Background() converge := func(context.Context, layout.Mode, bool) []layout.Result { return nil } t.Setenv(layoutDefaultEnv, "") armDefaultGate(t, errors.New("not yet run")) if !layoutLegWith(ctx, s, "kass-1", converge) { t.Fatal("a gate that is not ready must ask for a retry") } armDefaultGate(t, nil) if layoutLegWith(ctx, s, "kass-1", converge) { t.Fatal("a ready box with nothing deferred must not ask for a retry") } t.Setenv(layoutDefaultEnv, "flat") if layoutLegWith(ctx, s, "kass-1", converge) { t.Fatal("the opt-out waits on nothing") } t.Setenv(layoutDefaultEnv, "nestd") if layoutLegWith(ctx, s, "kass-1", converge) { t.Fatal("a typo waits on nothing") } }