| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "github.com/carlosframework/platform/internal/hostbox" |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/hostguard" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "reflect" |
| 14 | "testing" |
| 15 | ) |
| 16 | |
| 17 | func TestPlan14ServingHandoverPins(t *testing.T) { |
| 18 | f := newServingRootFixture(t, true) |
| 19 | root := servingHandoverRoot(t, f) |
| 20 | f.mutateRoot(func(m map[string]any) { |
| 21 | m["Installation"].(map[string]any)["Artifacts"] = map[string]any{"Schema": 1, "StateDirectory": true} |
| 22 | }) |
| 23 | if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" { |
| 24 | root.Reconcile.Retirement.JournalDir = claimRetireJournalDir("", "") |
| 25 | } |
| 26 | readHandovers := func() ([]hostbox.GenerationHandover, error) { |
| 27 | if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" { |
| 28 | return productionGenerationHandovers(t.Context()) |
| 29 | } |
| 30 | return generationReadHandovers(t.Context(), &rollbackDisk{dir: filepath.Join(root.Reconcile.Retirement.JournalDir, "serving")}) |
| 31 | } |
| 32 | next := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: f.actor.Binding, ETag: f.f.read().ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session} |
| 33 | if err := root.servingHandover().Run(t.Context(), f.receipt.Target, next); err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | installs, err := f.f.guard.Root.GenerationInstallations(t.Context()) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | payloads, err := f.f.journal.GenerationPayloads(t.Context()) |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | snapshot := generationInventory{Installations: installs, Payloads: payloads} |
| 45 | snapshot.Handovers, err = readHandovers() |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | |
| 50 | if _, err = generationTransfers(t.Context(), snapshot); err != nil { |
| 51 | t.Fatal("transfers", err) |
| 52 | } |
| 53 | if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { |
| 54 | t.Fatal("registry after actual serving handover", err) |
| 55 | } |
| 56 | physical, err := generationPhysicalPayloads(t.Context(), snapshot) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | if physical[0].Inventory.Entries[0].Identity != physical[1].Inventory.Entries[0].Identity { |
| 61 | t.Fatal("predecessor did not follow verified persistent identity") |
| 62 | } |
| 63 | t.Run("recreated-namespace", func(t *testing.T) { |
| 64 | raw, _ := json.Marshal(snapshot) |
| 65 | var recreated generationInventory |
| 66 | if err := json.Unmarshal(raw, &recreated); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | for i, e := range recreated.Payloads[0].Inventory.Entries { |
| 70 | if e.Kind != "path" { |
| 71 | continue |
| 72 | } |
| 73 | current := recreated.Payloads[1].Inventory.Entries[i].Identity |
| 74 | old := current |
| 75 | old.Mount += 100 |
| 76 | recreated.Payloads[0].Inventory.Entries[i].Identity = old |
| 77 | recreated.Handovers[0].Inventory.Entries[i].Identity = old |
| 78 | } |
| 79 | if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, recreated); err != nil { |
| 80 | t.Fatal("recreated namespace transfer", err) |
| 81 | } |
| 82 | if _, err := generationPhysicalPayloads(t.Context(), recreated); err != nil { |
| 83 | t.Fatal("recreated namespace payloads", err) |
| 84 | } |
| 85 | }) |
| 86 | original, _ := json.Marshal(snapshot) |
| 87 | for _, attack := range []string{"missing journal", "prepared", "successor", "barrier", "receipt", "retired", "path", "row", "revision", "duplicate", "cycle", "old boot"} { |
| 88 | t.Run(attack, func(t *testing.T) { |
| 89 | var bad generationInventory |
| 90 | if err := json.Unmarshal(original, &bad); err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | switch attack { |
| 94 | case "missing journal": |
| 95 | bad.Handovers = nil |
| 96 | case "prepared": |
| 97 | bad.Handovers[0].Phase = "prepared" |
| 98 | case "successor": |
| 99 | bad.Handovers[0].Successor.Stamp++ |
| 100 | case "barrier": |
| 101 | bad.Handovers[0].Barrier.ControllerID = "other" |
| 102 | case "receipt": |
| 103 | bad.Handovers[0].Receipt.Bytes = []byte("other") |
| 104 | case "retired": |
| 105 | bad.Installations[0].Retired = nil |
| 106 | case "path": |
| 107 | bad.Payloads[1].Inventory.Entries[0].Inode++ |
| 108 | case "row": |
| 109 | bad.Payloads[1].Inventory.Entries[1].RowIdentity = "other" |
| 110 | case "revision": |
| 111 | bad.Payloads[0].Revision++ |
| 112 | case "duplicate": |
| 113 | bad.Handovers = append(bad.Handovers, bad.Handovers[0]) |
| 114 | case "cycle": |
| 115 | bad.Handovers[0].Successor.Stamp = bad.Handovers[0].Prior.Stamp |
| 116 | case "old boot": |
| 117 | bad.Payloads[1].Inventory.Entries[0].Identity.Boot = "other-boot" |
| 118 | } |
| 119 | if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, bad); err == nil { |
| 120 | t.Fatal("accepted", attack) |
| 121 | } |
| 122 | }) |
| 123 | } |
| 124 | before := f.f.read() |
| 125 | receipt := servingStop(t, f.f, before, f.receipt.Target.BootID) |
| 126 | admitted, err := hostclaim.Update(t.Context(), f.f.claims, before, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: before.Claim.Holder.Session, ObservedGeneration: before.Claim.Holder.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"})) |
| 127 | if err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | next.ETag, next.Generation, next.Stamp, next.Session = admitted.ETag, admitted.Claim.Holder.Generation, admitted.Claim.Holder.Stamp, admitted.Claim.Holder.Session |
| 131 | if err = root.servingHandover().Run(t.Context(), receipt.Target, next); err != nil { |
| 132 | t.Fatal("second transfer", err) |
| 133 | } |
| 134 | snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context()) |
| 135 | if err != nil { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | snapshot.Payloads, err = f.f.journal.GenerationPayloads(t.Context()) |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | snapshot.Handovers, err = readHandovers() |
| 143 | if err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { |
| 147 | t.Fatal("second transfer registry", err) |
| 148 | } |
| 149 | if len(snapshot.Handovers) != 2 || len(snapshot.Payloads) != 3 { |
| 150 | t.Fatal("transfer lost retained authority") |
| 151 | } |
| 152 | physical, err = generationPhysicalPayloads(t.Context(), snapshot) |
| 153 | if err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | for i := range physical { |
| 157 | if !reflect.DeepEqual(physical[i].Inventory.Entries[0].Identity, physical[2].Inventory.Entries[0].Identity) { |
| 158 | t.Fatal("chain identity", i) |
| 159 | } |
| 160 | } |
| 161 | // The initial installation and stop evidence are fixtures. Transfers, root |
| 162 | // retirement, row removal and directory parking below use their production adapters. |
| 163 | stopped := servingStop(t, f.f, f.f.read(), f.receipt.Target.BootID) |
| 164 | terminal := snapshot.Payloads[2].Inventory |
| 165 | inventory := hostbox.RetirementInventory{Host: terminal.Host, Captured: hostguard.Captured{Incarnation: terminal.Incarnation, Stamp: terminal.InstallationStamp}, Ownership: terminal.Entries[0].Ownership} |
| 166 | for _, e := range terminal.Entries { |
| 167 | inventory.Artifacts = append(inventory.Artifacts, hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Identity: e.Identity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| 168 | } |
| 169 | retirement, err := hostbox.NewRetirementJournal(stopped.Target.FenceID, inventory) |
| 170 | if err != nil { |
| 171 | t.Fatal(err) |
| 172 | } |
| 173 | retirement.Target, retirement.StopReceipt = &stopped.Target, stopped.Bytes |
| 174 | data := f.f.guard.DataDir |
| 175 | t.Setenv("CARLOS_DATA_DIR", data) |
| 176 | if err = f.f.claims.Delete(t.Context(), snapshot.Installations[0].Installation.Record); err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | parker := hostbox.TypedParker{Roots: map[string]string{"data": data}, ParkedRoots: map[string]string{"data": data}, Incarnation: terminal.Incarnation, Host: terminal.Host, Retirement: retirement.ID} |
| 180 | runner := hostbox.TypedRetirement{JournalDir: root.Reconcile.Retirement.JournalDir, Authority: hostbox.InstallationCleanup{Guard: f.f.guard, Verify: verifyRetirementReceipt}, Filesystem: parker, Rows: hostbox.RegistryRows{Registry: root.Reconcile.Registry, Inventory: inventory}, Ack: func(context.Context, hostbox.RetirementJournal) error { return nil }} |
| 181 | if err = runner.Run(t.Context(), stopped.Target, retirement); err != nil { |
| 182 | t.Fatal("terminal retirement", err) |
| 183 | } |
| 184 | raw, err := os.ReadFile(filepath.Join(runner.JournalDir, retirement.ID+".json")) |
| 185 | if err != nil { |
| 186 | t.Fatal(err) |
| 187 | } |
| 188 | if err = json.Unmarshal(raw, &retirement); err != nil || !retirement.Complete { |
| 189 | t.Fatal(err) |
| 190 | } |
| 191 | snapshot.Retirements = []hostbox.RetirementJournal{retirement} |
| 192 | snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context()) |
| 193 | if err != nil { |
| 194 | t.Fatal(err) |
| 195 | } |
| 196 | if _, err = generationRequiredInventory(t.Context(), f.f.claims, snapshot, nil); err != nil { |
| 197 | t.Fatal("deleted record through transfer chain", err) |
| 198 | } |
| 199 | if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { |
| 200 | t.Fatal("retired transfer chain", err) |
| 201 | } |
| 202 | physical, err = generationPhysicalPayloads(t.Context(), snapshot) |
| 203 | if err != nil { |
| 204 | t.Fatal(err) |
| 205 | } |
| 206 | for _, payload := range physical { |
| 207 | for _, e := range payload.Inventory.Entries { |
| 208 | if e.Kind == "path" { |
| 209 | if _, err = os.Stat(filepath.Join(data, e.RelativePath)); err != nil { |
| 210 | t.Fatal("retained predecessor location", err) |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | } |
| 217 | |