| 1 | //go:build unix |
| 2 | |
| 3 | package hostguard |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "testing" |
| 12 | ) |
| 13 | |
| 14 | // Fix round 1, finding 1 (and round 2): an obsolete pending intent is settled into a |
| 15 | // quarantine the migration census may inspect, while every execution read |
| 16 | // still refuses it. A landed commit is recorded as the commit would have |
| 17 | // recorded it; an unlanded one leaves the record's installation. |
| 18 | func TestSettleObsoleteRootInventory(t *testing.T) { |
| 19 | for _, landed := range []bool{true, false} { |
| 20 | name := "unlanded" |
| 21 | if landed { |
| 22 | name = "landed" |
| 23 | } |
| 24 | t.Run(name, func(t *testing.T) { |
| 25 | ctx := t.Context() |
| 26 | f, target, captured := fenceFixture(t) |
| 27 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 28 | f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| 29 | 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} |
| 30 | if err := f.g.EstablishRoot(ctx, in, allow); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | before := in |
| 34 | in.InventoryRevision++ |
| 35 | fault := errors.New("interrupted") |
| 36 | if err := f.g.CommitRootInventory(ctx, in, []byte("exact"), allow, nil, func(RootInstallation) error { return fault }); !errors.Is(err, fault) { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if err := f.g.SettleObsoleteRootInventory(ctx, target.Host, []byte("forged"), func(context.Context, RootInstallation) (bool, error) { return landed, nil }); !errors.Is(err, ErrInvalidInput) { |
| 40 | t.Fatal("forged intent settled:", err) |
| 41 | } |
| 42 | var got RootInstallation |
| 43 | if err := f.g.SettleObsoleteRootInventory(ctx, target.Host, []byte("exact"), func(_ context.Context, p RootInstallation) (bool, error) { got = p; return landed, nil }); err != nil { |
| 44 | t.Fatal(err) |
| 45 | } |
| 46 | if got != in { |
| 47 | t.Fatalf("applied asked about %+v, want the pending %+v", got, in) |
| 48 | } |
| 49 | r, err := f.g.Root.read(target.Host) |
| 50 | if err != nil || !r.quarantined || !r.settled || r.pendingInstallation != nil { |
| 51 | t.Fatalf("after settlement: %+v %v", r, err) |
| 52 | } |
| 53 | want := before |
| 54 | if landed { |
| 55 | want = in |
| 56 | } |
| 57 | if r.installation != want { |
| 58 | t.Fatalf("installation %+v, want %+v", r.installation, want) |
| 59 | } |
| 60 | // Execution reads refuse; the census lists it, marked Settled. |
| 61 | if _, err := f.g.Root.View(ctx, target.Host, captured); !errors.Is(err, ErrFenced) { |
| 62 | t.Fatal("execution view of a settled installation:", err) |
| 63 | } |
| 64 | if err := r.admits(f.obs.Claim, captured); !errors.Is(err, ErrFenced) { |
| 65 | t.Fatal("admission of a settled installation:", err) |
| 66 | } |
| 67 | views, err := f.g.Root.MigrationInstallations(ctx) |
| 68 | if err != nil || len(views) != 1 || !views[0].Settled || views[0].Installation != want { |
| 69 | t.Fatalf("census: %+v %v", views, err) |
| 70 | } |
| 71 | // Settled without the quarantine is malformed. |
| 72 | raw, err := f.g.Root.store.Read(target.Host + ".json") |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | var saved map[string]any |
| 77 | if err := json.Unmarshal(raw, &saved); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | delete(saved, "Quarantined") |
| 81 | forged, _ := json.Marshal(saved) |
| 82 | if err := f.g.Root.store.Write(target.Host+".json", forged); err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | if _, err := f.g.Root.read(target.Host); !errors.Is(err, ErrMalformedRecord) { |
| 86 | t.Fatal("settled without quarantine:", err) |
| 87 | } |
| 88 | }) |
| 89 | } |
| 90 | |
| 91 | // The superseded-claim abandonment ends in the same terminal state (fix |
| 92 | // round 2): it keeps its exact-intent and fresh-claim checks, reconciles a |
| 93 | // landed commit, and leaves an installation the census can inspect and |
| 94 | // execution still refuses. |
| 95 | for _, landed := range []bool{true, false} { |
| 96 | name := "abandoned-unlanded" |
| 97 | if landed { |
| 98 | name = "abandoned-landed" |
| 99 | } |
| 100 | t.Run(name, func(t *testing.T) { |
| 101 | ctx := t.Context() |
| 102 | f, target, captured := fenceFixture(t) |
| 103 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 104 | f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| 105 | 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} |
| 106 | if err := f.g.EstablishRoot(ctx, in, allow); err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | before := in |
| 110 | in.InventoryRevision++ |
| 111 | fault := errors.New("interrupted") |
| 112 | if err := f.g.CommitRootInventory(ctx, in, []byte("exact"), allow, nil, func(RootInstallation) error { return fault }); !errors.Is(err, fault) { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | answer := func(context.Context, RootInstallation) (bool, error) { return landed, nil } |
| 116 | if err := f.g.AbandonRootInventory(ctx, target.Host, []byte("forged"), func(*hostclaim.Claim) bool { return true }, answer); !errors.Is(err, ErrInvalidInput) { |
| 117 | t.Fatal("forged intent abandoned:", err) |
| 118 | } |
| 119 | if err := f.g.AbandonRootInventory(ctx, target.Host, []byte("exact"), func(*hostclaim.Claim) bool { return false }, answer); !errors.Is(err, ErrFenced) { |
| 120 | t.Fatal("unsuperseded intent abandoned:", err) |
| 121 | } |
| 122 | if pending, err := f.g.Root.PendingInstallations(); err != nil || len(pending) != 1 { |
| 123 | t.Fatalf("refused abandonment changed the intent: %d %v", len(pending), err) |
| 124 | } |
| 125 | if err := f.g.AbandonRootInventory(ctx, target.Host, []byte("exact"), func(*hostclaim.Claim) bool { return true }, answer); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | r, err := f.g.Root.read(target.Host) |
| 129 | if err != nil || !r.quarantined || !r.settled || r.pendingInstallation != nil { |
| 130 | t.Fatalf("after abandonment: %+v %v", r, err) |
| 131 | } |
| 132 | want := before |
| 133 | if landed { |
| 134 | want = in |
| 135 | } |
| 136 | if r.installation != want { |
| 137 | t.Fatalf("installation %+v, want %+v", r.installation, want) |
| 138 | } |
| 139 | if _, err := f.g.Root.View(ctx, target.Host, captured); !errors.Is(err, ErrFenced) { |
| 140 | t.Fatal("execution view of an abandoned installation:", err) |
| 141 | } |
| 142 | if err := r.admits(f.obs.Claim, captured); !errors.Is(err, ErrFenced) { |
| 143 | t.Fatal("admission of an abandoned installation:", err) |
| 144 | } |
| 145 | views, err := f.g.Root.MigrationInstallations(ctx) |
| 146 | if err != nil || len(views) != 1 || !views[0].Settled || views[0].Installation != want { |
| 147 | t.Fatalf("census of an abandoned installation: %+v %v", views, err) |
| 148 | } |
| 149 | }) |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | // unlanded is an applied answer for an intent whose commit never landed. |
| 154 | func unlanded(context.Context, RootInstallation) (bool, error) { return false, nil } |
| 155 | |