//go:build unix package hostguard import ( "context" "errors" "os" "path/filepath" "testing" "time" ) func TestAdmissionFencedFollowsTheDurableFence(t *testing.T) { f, target, captured := fenceFixture(t) if fenced, err := f.g.AdmissionFenced(target.Host, f.obs.Claim, captured); err != nil || fenced { t.Fatalf("before any fence: %v %v", fenced, err) } if _, err := f.g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } // Read inside the host lock, as Activate's reader and WithLocal do: // no second acquisition, no deadlock. err := f.g.WithLocal(t.Context(), target.Host, func() error { fenced, err := f.g.AdmissionFenced(target.Host, f.obs.Claim, captured) if err != nil || !fenced { return errors.New("after BeginFence, under the lock: not fenced") } return nil }) if err != nil { t.Fatal(err) } // A capture whose stamp is not the active one is refused by With, so // it reads closed too. other := captured other.Stamp++ if fenced, err := f.g.AdmissionFenced(target.Host, f.obs.Claim, other); err != nil || !fenced { t.Fatalf("another installation: %v %v", fenced, err) } if _, err := f.g.AdmissionFenced("no.such.host", f.obs.Claim, captured); err == nil { t.Fatal("a claim for another host was read") } } // AdmissionFenced and With read the same record the same way: With admits // exactly when AdmissionFenced reads open with no error, in every state. func TestAdmissionFencedAgreesWithWith(t *testing.T) { verified := func(context.Context, FenceTarget, []byte, time.Time) error { return nil } complete := func(t *testing.T, f *guardFixture, target FenceTarget) { if _, err := f.g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } if err := f.g.CompleteFence(t.Context(), target, []byte("root receipt"), time.Unix(100, 0).UTC(), verified); err != nil { t.Fatal(err) } } retire := func(t *testing.T, f *guardFixture, target FenceTarget) { complete(t, f, target) if err := f.g.CleanupRetireInstallationWith(t.Context(), target, func(context.Context, FenceReceipt) error { return nil }, func() error { return nil }); err != nil { t.Fatal(err) } } for _, tc := range []struct { name string change func(*testing.T, *guardFixture, FenceTarget, *Captured) with error // With's refusal; nil admits fenced bool readErr error }{ {"open", func(*testing.T, *guardFixture, FenceTarget, *Captured) {}, nil, false, nil}, {"a stop root began", func(t *testing.T, f *guardFixture, target FenceTarget, _ *Captured) { if _, err := f.g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } }, ErrFenced, true, nil}, {"a stop root completed, the receipt only", func(t *testing.T, f *guardFixture, target FenceTarget, _ *Captured) { complete(t, f, target) // As CompleteColdHandover leaves it: no active fence, the journal kept. r, err := readRecord(f.g.DataDir, target.Host) if err != nil { t.Fatal(err) } r.ActiveFence = nil if err := writeRecord(f.g.DataDir, r); err != nil { t.Fatal(err) } }, ErrFenced, true, nil}, {"fenced, then retired by cleanup", func(t *testing.T, f *guardFixture, target FenceTarget, _ *Captured) { retire(t, f, target) }, ErrInactive, true, nil}, {"fenced, retired, then captured again", func(t *testing.T, f *guardFixture, target FenceTarget, c *Captured) { retire(t, f, target) fresh, err := Capture(f.g.DataDir, target.Host) if err != nil { t.Fatal(err) } *c = fresh }, ErrInactive, true, nil}, {"retired with no fence", func(t *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { if err := f.g.RetireInstallation(t.Context(), f.obs.Host, allow); err != nil { t.Fatal(err) } }, ErrInactive, true, nil}, {"a later installation is active", func(t *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { f.obs.Claim.Holder.Stamp = 3 if err := f.g.Activate(t.Context(), f.obs.Host, "uk", nil); err != nil { t.Fatal(err) } }, ErrStampMismatch, true, nil}, {"another incarnation on the record", func(t *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { if err := writeRecord(f.g.DataDir, record{V: 1, Host: f.obs.Host, Incarnation: "other", ActiveStamp: 1}); err != nil { t.Fatal(err) } }, ErrRecordIncarnation, true, nil}, {"a later session of the fenced installation", func(t *testing.T, f *guardFixture, target FenceTarget, _ *Captured) { if _, err := f.g.BeginFence(t.Context(), target); err != nil { t.Fatal(err) } f.obs.Claim.Holder.Session = 8 }, nil, false, nil}, {"a claim of another incarnation", func(_ *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { f.obs.Claim.Holder.Incarnation = "other" }, ErrClaimIncarnation, false, ErrClaimIncarnation}, {"a claim of another host", func(_ *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { f.obs.Claim.Host = "else.example" }, ErrMalformedClaim, false, ErrMalformedClaim}, {"no record", func(t *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { if err := os.Remove(filepath.Join(f.g.DataDir, "hostguard", f.obs.Host+".json")); err != nil { t.Fatal(err) } }, ErrNoRecord, false, ErrNoRecord}, {"a malformed record", func(t *testing.T, f *guardFixture, _ FenceTarget, _ *Captured) { if err := os.WriteFile(filepath.Join(f.g.DataDir, "hostguard", f.obs.Host+".json"), []byte(`{`), 0660); err != nil { t.Fatal(err) } }, ErrMalformedRecord, false, ErrMalformedRecord}, } { t.Run(tc.name, func(t *testing.T) { f, target, captured := fenceFixture(t) tc.change(t, f, target, &captured) called := false withErr := f.g.With(t.Context(), f.obs.Host, captured, allow, func() error { called = true; return nil }) if !errors.Is(withErr, tc.with) || (withErr == nil) != called { t.Fatalf("With = %v, called=%v; want %v", withErr, called, tc.with) } fenced, err := f.g.AdmissionFenced(f.obs.Host, f.obs.Claim, captured) if fenced != tc.fenced || !errors.Is(err, tc.readErr) || (tc.readErr == nil && err != nil) { t.Fatalf("AdmissionFenced = %v, %v; want %v, %v", fenced, err, tc.fenced, tc.readErr) } if open := !fenced && err == nil; open != (withErr == nil) { t.Fatalf("disagreement: With = %v, AdmissionFenced = %v, %v", withErr, fenced, err) } }) } }