| 1 | //go:build unix |
| 2 | |
| 3 | package hostguard |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "errors" |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "testing" |
| 11 | "time" |
| 12 | ) |
| 13 | |
| 14 | func TestPlan14Fix2LegacyLockCannotStallRoot(t *testing.T) { |
| 15 | f, target, captured := fenceFixture(t) |
| 16 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 17 | f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| 18 | in := RootInstallation{Host: target.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: target.Box, Enrollment: target.Enrollment, Migration: 1} |
| 19 | if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | unlock, err := acquire(t.Context(), f.g.LockDir, target.Host) |
| 23 | if err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | defer func() { |
| 27 | if unlock != nil { |
| 28 | unlock() |
| 29 | } |
| 30 | }() |
| 31 | before, err := f.g.Root.store.Read(target.Host + ".json") |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | ctx, cancel := context.WithTimeout(t.Context(), time.Second) |
| 36 | defer cancel() |
| 37 | if _, err := f.g.PendingFences(ctx); err != nil { |
| 38 | t.Fatal("read waited for legacy lock", err) |
| 39 | } |
| 40 | if _, err := f.g.BeginFence(ctx, target); err == nil { |
| 41 | t.Fatal("mutation crossed held legacy lock") |
| 42 | } |
| 43 | r, err := f.g.Root.read(target.Host) |
| 44 | if err != nil || r.quarantined { |
| 45 | t.Fatalf("honest contention quarantined: %+v %v", r, err) |
| 46 | } |
| 47 | after, err := f.g.Root.store.Read(target.Host + ".json") |
| 48 | if err != nil || !bytes.Equal(before, after) { |
| 49 | t.Fatal("contention wrote durable state", err) |
| 50 | } |
| 51 | if _, err := f.g.Root.View(ctx, target.Host, captured); err != nil { |
| 52 | t.Fatal("contention fenced running installation", err) |
| 53 | } |
| 54 | unlock() |
| 55 | unlock = nil |
| 56 | if _, err := f.g.BeginFence(ctx, target); err != nil { |
| 57 | t.Fatal("released contention did not retry", err) |
| 58 | } |
| 59 | } |
| 60 | func TestPlan14Fix2PendingIntentCanBeAbandoned(t *testing.T) { |
| 61 | f, target, captured := fenceFixture(t) |
| 62 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 63 | f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| 64 | in := RootInstallation{Host: target.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: target.Box, Enrollment: target.Enrollment, Migration: 1, InventoryRevision: 1} |
| 65 | intent := []byte("pending") |
| 66 | failure := errors.New("commit failed") |
| 67 | if err := f.g.CommitRootInventory(t.Context(), in, intent, allow, nil, func(RootInstallation) error { return failure }); !errors.Is(err, failure) { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | if err := f.g.AbandonRootInventory(t.Context(), target.Host, intent, func(*hostclaim.Claim) bool { return false }, unlanded); !errors.Is(err, ErrFenced) { |
| 71 | t.Fatal("abandoned committable intent", err) |
| 72 | } |
| 73 | if err := f.g.AbandonRootInventory(t.Context(), target.Host, intent, func(*hostclaim.Claim) bool { return true }, unlanded); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | pending, err := f.g.Root.PendingInstallations() |
| 77 | if err != nil || len(pending) != 0 { |
| 78 | t.Fatal(pending, err) |
| 79 | } |
| 80 | if _, err := f.g.Root.View(t.Context(), target.Host, captured); !errors.Is(err, ErrFenced) { |
| 81 | t.Fatal("abandon discarded deny", err) |
| 82 | } |
| 83 | if err := f.g.CommitRootInventory(t.Context(), in, []byte("new"), allow, nil, func(RootInstallation) error { return nil }); err != nil { |
| 84 | t.Fatal("fresh intent refused", err) |
| 85 | } |
| 86 | if _, err := f.g.Root.View(t.Context(), target.Host, captured); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | func TestPlan14Fix2SupersededColdBarrierAbandoned(t *testing.T) { |
| 92 | f, prior, captured := fenceFixture(t) |
| 93 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 94 | f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| 95 | in := RootInstallation{Host: prior.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: prior.Box, Enrollment: prior.Enrollment, Migration: 1} |
| 96 | if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | f.obs.Claim.Op.Kind = hostclaim.KindMoving |
| 100 | if _, err := f.g.BeginFence(t.Context(), prior); err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | if err := f.g.CompleteFence(t.Context(), prior, []byte("receipt"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | barrier := prior |
| 107 | barrier.FenceID = "barrier" |
| 108 | barrier.Stamp = 17 |
| 109 | barrier.Session = 17 |
| 110 | f.obs.Claim.Holder.Stamp, f.obs.Claim.Holder.Session = 17, 17 |
| 111 | if err := f.g.BeginColdHandover(t.Context(), prior, barrier, allow); err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
| 114 | if err := f.g.AbandonColdHandover(t.Context(), barrier); !errors.Is(err, ErrFenced) { |
| 115 | t.Fatal("abandoned live admission", err) |
| 116 | } |
| 117 | f.obs.Claim.Holder.Stamp, f.obs.Claim.Holder.Session = 18, 18 |
| 118 | if err := f.g.AbandonColdHandover(t.Context(), barrier); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | r, err := f.g.Root.read(prior.Host) |
| 122 | if err != nil || r.barrier != nil || len(r.FenceJournal) != 1 || r.FenceJournal[0].Target != prior { |
| 123 | t.Fatalf("barrier/receipt: %+v %v", r, err) |
| 124 | } |
| 125 | } |
| 126 | |