| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostactors" |
| 11 | "github.com/carlosframework/platform/internal/hostbox" |
| 12 | ) |
| 13 | |
| 14 | // N2 (roster binding, task 3b). A serving park's reboot preparation persists |
| 15 | // the actor it was prepared under and its commit compares that record with a |
| 16 | // fresh one. Root's inventory actor here is the production identity over a |
| 17 | // signed roster, and every read of it follows another console create's hold |
| 18 | // churn (a real Reserve and ReleaseFinishedHold), so the preparation's reread, |
| 19 | // its persisted copy and the commit all see a different roster revision. They |
| 20 | // still match: the preparation stores the bound actor. A quarantine and a |
| 21 | // readmission between prepare and commit change the binding and are refused |
| 22 | // (the quarantine by Verify, the readmission by the installation's stamp). |
| 23 | func TestServingRebootPreparationSurvivesHoldChurn(t *testing.T) { |
| 24 | for _, step := range []string{"hold", "hold-root-restart", "quarantine-readmit"} { |
| 25 | t.Run(step, func(t *testing.T) { |
| 26 | ctx := t.Context() |
| 27 | roster := newBindingRosterFixture(t, "box-1", "ie") |
| 28 | identity := roster.identity() |
| 29 | actor, err := identity.Verify(ctx) |
| 30 | if err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | if !hostactors.ValidBinding(actor.Binding) || actor.Binding != roster.binding() { |
| 34 | t.Fatalf("production actor binding %q", actor.Binding) |
| 35 | } |
| 36 | f, r := servingRebootPreparationFixtureFor(t, "parked", actor) |
| 37 | churning := func(identity hostbox.IdentityProvider) func(context.Context) (hostbox.Actor, error) { |
| 38 | return func(ctx context.Context) (hostbox.Actor, error) { |
| 39 | roster.holdChurn() |
| 40 | return identity.Verify(ctx) |
| 41 | } |
| 42 | } |
| 43 | f.p.root.InventoryActor = churning(identity) |
| 44 | id := r.Authorization.ID |
| 45 | before := roster.read().Roster.Revision |
| 46 | if err := f.p.prepareServingReboot(ctx, id); err != nil { |
| 47 | t.Fatal("prepare under hold churn:", err) |
| 48 | } |
| 49 | if after := roster.read().Roster.Revision; after < before+4 { |
| 50 | t.Fatalf("prepare read the actor without churn: revision %d -> %d", before, after) |
| 51 | } |
| 52 | body, err := f.disk.Read(servingRebootPreparationName(id, servingOtherBoot)) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | var prep servingParkRebootPreparation |
| 57 | if servingParkDecode(body, &prep) != nil || prep.Active == nil || prep.Active.Actor != actor.Bound() || prep.Active.Actor.Revision != 0 { |
| 58 | t.Fatalf("persisted preparation actor: %+v", prep.Active) |
| 59 | } |
| 60 | roster.holdChurn() |
| 61 | p := f.p |
| 62 | switch step { |
| 63 | case "hold-root-restart": |
| 64 | // A restarted root: a new ledger and handler over the same |
| 65 | // private state, with a newly built identity. |
| 66 | ledger := *f.p.ledger |
| 67 | root := f.p.root |
| 68 | root.InventoryActor = churning(roster.identity()) |
| 69 | p = &servingParkRoot{root: root, ledger: &ledger, empty: f.p.empty} |
| 70 | case "quarantine-readmit": |
| 71 | roster.quarantine("loss_1") |
| 72 | if err := p.commitServingReboot(ctx, id); err == nil { |
| 73 | t.Fatal("commit accepted while quarantined") |
| 74 | } |
| 75 | roster.readmit() |
| 76 | if b := roster.binding(); b == actor.Binding { |
| 77 | t.Fatal("readmission kept the binding") |
| 78 | } |
| 79 | } |
| 80 | err = p.commitServingReboot(ctx, id) |
| 81 | if step == "quarantine-readmit" { |
| 82 | // The installation's stamp is the old binding, so the fresh |
| 83 | // installed-state check (claimservingparksource_linux.go, the |
| 84 | // inventory against actor.Binding) refuses before the persisted |
| 85 | // preparation is compared: ErrTypedInventory, not ErrChanged. |
| 86 | if !errors.Is(err, hostbox.ErrTypedInventory) { |
| 87 | t.Fatalf("commit after readmission: %v, want ErrTypedInventory", err) |
| 88 | } |
| 89 | if _, err := f.disk.Read("current-namespace.json"); err == nil { |
| 90 | t.Fatal("refused commit bound the namespace") |
| 91 | } |
| 92 | return |
| 93 | } |
| 94 | if err != nil { |
| 95 | t.Fatal("commit after hold churn:", err) |
| 96 | } |
| 97 | fs := p.ledger.fs.(*servingParkFS) |
| 98 | if err := fs.recoverRecordedNamespace(ctx); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | current, err := p.ledger.read(id) |
| 102 | if err != nil || current.Source.BootID != servingOtherBoot { |
| 103 | t.Fatal("committed authority", err) |
| 104 | } |
| 105 | }) |
| 106 | } |
| 107 | } |
| 108 | |