carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "encoding/json"
8 "net"
9 "os"
10 "path/filepath"
11 "reflect"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/hostwrite"
19 "github.com/carlosframework/platform/internal/instancectl"
20 "github.com/carlosframework/platform/internal/provision"
21 "github.com/carlosframework/platform/internal/registry"
22)
23
24// Initial installation remains a fixture. Every later ownership transfer uses
25// the production serving handover and retains the original source directory.
26func servingHandoverRoot(t *testing.T, f *servingRootFixture) ClaimRootHTTP {
27 t.Helper()
28 inventory := f.p.root.Journals.Inventory
29 captured, err := f.f.guard.Capture(f.f.read().Host)
30 if err != nil {
31 t.Fatal(err)
32 }
33 in, _, err := inventory.Snapshot(t.Context(), f.f.read().Host, captured)
34 if err != nil {
35 t.Fatal(err)
36 }
37 if f.p.root.Reconcile.Registry == nil {
38 owner := registry.ClaimOwnership{ClaimIncarnation: in.Incarnation, ClaimStamp: in.InstallationStamp, ClaimOwner: in.Entries[0].Ownership, ClaimRowID: "prior-route"}
39 reg := restartTestRegistry(t)
40 holder := f.f.read().Claim.Holder
41 row := registry.Route{Host: in.Host, Account: holder.Account, App: holder.App, Kind: "instance", DBPath: f.db, Socket: provision.NestedSocket(in.Host), ClaimOwnership: owner}
42 if err := reg.Upsert(row); err != nil {
43 t.Fatal(err)
44 }
45 in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: in.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"})
46 if err := inventory.WriteStopInventory(t.Context(), in); err != nil {
47 t.Fatal(err)
48 }
49 _, revision, err := inventory.Snapshot(t.Context(), in.Host, captured)
50 if err != nil {
51 t.Fatal(err)
52 }
53 raw, err := f.rootDisk.Read(in.Host + ".json")
54 if err != nil {
55 t.Fatal(err)
56 }
57 var saved map[string]any
58 if err = json.Unmarshal(raw, &saved); err != nil {
59 t.Fatal(err)
60 }
61 saved["Installation"].(map[string]any)["InventoryRevision"] = revision
62 raw, err = json.Marshal(saved)
63 if err != nil {
64 t.Fatal(err)
65 }
66 if err = f.rootDisk.Write(in.Host+".json", raw); err != nil {
67 t.Fatal(err)
68 }
69 record := provision.Record{Schema: 1, Host: in.Host, Account: holder.Account, App: holder.App, Region: f.actor.Region, CreatedAt: holder.CreatedAt, Channel: "edge"}
70 body, err := json.Marshal(record)
71 if err != nil {
72 t.Fatal(err)
73 }
74 if _, err = f.f.claims.Put(t.Context(), holder.Key, body, ""); err != nil {
75 t.Fatal(err)
76 }
77 f.p.root.Reconcile.Registry = reg
78 f.p.root.Reconcile.Inventory = inventory
79 f.p.root.Reconcile.Box, f.p.root.Reconcile.Enrollment = f.actor.Box, f.actor.Enrollment
80 f.p.root.Reconcile.Retirement.JournalDir = filepath.Join(t.TempDir(), "handover")
81 f.p.root.Launches = f.f.journal
82 }
83 f.f.guard.Root = hostguard.NewRootAuthorityWithInventory(f.rootDisk, func(ctx context.Context, host string, c hostguard.Captured) (uint64, error) {
84 _, rev, err := inventory.Snapshot(ctx, host, c)
85 return rev, err
86 })
87 f.p.root.Stop.Coordinator.Guard = f.f.guard
88 f.f.evidence = f.f.claims
89 f.p.root.Stop.Coordinator.Evidence = f.f.claims
90 return servingTransportRoot(t, f)
91}
92
93func TestServingParkColdHandoverPreservesState(t *testing.T) { runServingColdHandoverBoundary(t, nil) }
94
95func TestServingHandoverThenPrelaunchParkReplay(t *testing.T) {
96 f := newServingRootFixture(t, true)
97 root := servingHandoverRoot(t, f)
98 socket := servingRootSocket(t, root)
99 hook := coldHandoverWith(handoverRecoveryIdentity(t, f.actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
100 return hostguard.FenceReceipt{Target: f.receipt.Target}, nil
101 })
102 admitted := f.f.read()
103 for i := 0; i < 2; i++ {
104 if err := hook(t.Context(), f.predecessor, admitted); err != nil {
105 t.Fatal("handover", err)
106 }
107 }
108 installed, err := f.f.guard.Capture(f.current.Host)
109 if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) {
110 t.Fatalf("installation: %+v %v", installed, err)
111 }
112 target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
113 if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
114 t.Fatal(err)
115 }
116 client := hostbox.ServingParkClient{Call: rootJournalClient(socket)}
117 a, err := client.Park(t.Context(), f.current, f.prior)
118 if err != nil {
119 t.Fatal("park after prelaunch", err)
120 }
121 f.tags(f.current.Stamp)
122 if err := client.Complete(t.Context(), a.ID, f.current); err != nil {
123 t.Fatal(err)
124 }
125 before, err := f.p.ledger.read(a.ID)
126 if err != nil || !before.Complete {
127 t.Fatal("completion", err)
128 }
129 again, err := client.Park(t.Context(), f.current, f.prior)
130 if err != nil || again != a {
131 t.Fatalf("replay after handover and prelaunch: %+v %v", again, err)
132 }
133 after, err := f.p.ledger.read(a.ID)
134 if err != nil || !reflect.DeepEqual(before, after) {
135 t.Fatal("replay changed record", err)
136 }
137 if err := f.p.ledger.fs.CheckParked(&after); err != nil {
138 t.Fatal(err)
139 }
140}
141
142func runServingColdHandoverBoundary(t *testing.T, mount func(*testing.T, string), revisions ...uint64) {
143 t.Helper()
144 f := newServingRootFixture(t, true)
145 if mount != nil {
146 mount(t, f.f.guard.DataDir)
147 }
148 root := servingHandoverRoot(t, f)
149 if len(revisions) != 0 {
150 f.actor.Revision = revisions[0]
151 }
152 stateDir := filepath.Dir(f.db)
153 expected := map[string]string{filepath.Base(f.db): "served bytes", filepath.Base(f.db) + ".gen": "G", filepath.Base(f.db) + "-wal": "WAL", filepath.Base(f.db) + "-shm": "SHM", "files/upload": "tenant upload", "env": "tenant configuration"}
154 for _, name := range []string{"files/upload", "env"} {
155 p := filepath.Join(stateDir, name)
156 if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
157 t.Fatal(err)
158 }
159 if err := os.WriteFile(p, []byte(expected[name]), 0600); err != nil {
160 t.Fatal(err)
161 }
162 }
163 before := f.f.read()
164 old, _, err := root.Reconcile.Inventory.Snapshot(t.Context(), before.Host, hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp})
165 if err != nil {
166 t.Fatal(err)
167 }
168 next := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: f.actor.Binding, ETag: before.ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session}
169 handover := root.servingHandover()
170 for i := 0; i < 2; i++ {
171 if err = handover.Run(t.Context(), f.receipt.Target, next); err != nil {
172 t.Fatal("serving handover", err)
173 }
174 }
175 for name, want := range expected {
176 body, err := os.ReadFile(filepath.Join(stateDir, name))
177 if err != nil || string(body) != want {
178 t.Fatalf("handover changed %s: %q %v", name, body, err)
179 }
180 }
181 retained, _, err := root.Reconcile.Inventory.Snapshot(t.Context(), before.Host, hostguard.Captured{Incarnation: f.prior.Incarnation, Stamp: f.prior.Stamp})
182 if err != nil || !reflect.DeepEqual(old, retained) {
183 t.Fatal("predecessor inventory changed", err)
184 }
185 if f.f.read().ETag != before.ETag {
186 t.Fatal("handover rewrote claim")
187 }
188 a, err := f.park()
189 if err != nil {
190 t.Fatal("database retirement after handover", err)
191 }
192 f.tags(f.current.Stamp)
193 if err = f.p.completeServingDatabasePark(t.Context(), f.conn, a.ID, f.current); err != nil {
194 t.Fatal(err)
195 }
196 for _, name := range []string{"files/upload", "env"} {
197 body, err := os.ReadFile(filepath.Join(stateDir, name))
198 if err != nil || string(body) != expected[name] {
199 t.Fatalf("retirement changed %s: %q %v", name, body, err)
200 }
201 }
202}
203
204func TestServingHandoverLegacy(t *testing.T) {
205 f, _, coordinator := newServingLegacyRootSetup(t, false)
206 root := servingHandoverRoot(t, f)
207 boot, err := servingParkBootID()
208 if err != nil {
209 t.Fatal(err)
210 }
211 f.f.peer.BootID = boot
212 o := f.f.read()
213 req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: o.Claim.Holder.Incarnation, InstallationStamp: o.Claim.Holder.Stamp, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: boot, ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7", RosterRevision: 7}
214 if _, err := coordinator.FenceAndStop(t.Context(), req); err != nil {
215 t.Fatal(err)
216 }
217 admitted, err := hostclaim.Update(t.Context(), f.f.claims, f.f.read(), hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: 0, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G"}))
218 if err != nil {
219 t.Fatal(err)
220 }
221 f.current, err = servingWriterIdentity(admitted, f.actor.Box, f.actor.Region)
222 if err != nil {
223 t.Fatal(err)
224 }
225 saved, err := f.p.ledger.legacyStop(f.current)
226 if err != nil {
227 t.Fatal(err)
228 }
229 f.actor.Revision = 8
230 n := 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}
231 if err := root.servingHandover().Run(t.Context(), saved.Result.Target, n); err != nil {
232 t.Fatalf("legacy transfer: %v prior=%+v next=%+v", err, saved.Result.Target, n)
233 }
234 if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err != nil {
235 t.Fatal("legacy park after transfer", err)
236 }
237}
238
239func TestServingHandoverRefusesForeignAuthority(t *testing.T) {
240 for _, attack := range []string{"census", "actor", "successor", "directory-revision", "operation", "predecessor", "receipt", "root-revision", "registry-path", "registry-account", "source-replaced", "extra-timer", "prelaunch"} {
241 t.Run(attack, func(t *testing.T) {
242 f := newServingRootFixture(t, true)
243 root := servingHandoverRoot(t, f)
244 p := f.receipt.Target
245 n := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: f.actor.Box, Enrollment: f.actor.Enrollment, DirectoryRevision: "7", ETag: f.f.read().ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session}
246 row, err := root.Reconcile.Registry.Get(n.Host)
247 if err != nil {
248 t.Fatal(err)
249 }
250 switch attack {
251 case "census":
252 root.RequireNewWork = func(context.Context) error { return hostbox.ErrChanged }
253 case "actor":
254 f.actor.Eligible = false
255 case "successor":
256 n.Generation = "foreign"
257 case "directory-revision":
258 n.DirectoryRevision = "8"
259 case "operation":
260 n.Op = "move"
261 case "predecessor":
262 p.Session++
263 case "receipt":
264 p.FenceID = "unknown"
265 case "root-revision":
266 f.mutateRoot(func(m map[string]any) { m["Installation"].(map[string]any)["InventoryRevision"] = 999 })
267 case "registry-path":
268 row.DBPath = filepath.Join(t.TempDir(), "victim.db")
269 if err = root.Reconcile.Registry.Upsert(*row); err != nil {
270 t.Fatal(err)
271 }
272 case "registry-account":
273 row.Account = "bac"
274 if err = root.Reconcile.Registry.Upsert(*row); err != nil {
275 t.Fatal(err)
276 }
277 case "source-replaced":
278 dir := filepath.Dir(f.db)
279 if err = os.Rename(dir, dir+"-original"); err != nil {
280 t.Fatal(err)
281 }
282 if err = os.Mkdir(dir, 0700); err != nil {
283 t.Fatal(err)
284 }
285 case "extra-timer":
286 if err = root.Reconcile.Registry.UpsertTimer(registry.Timer{Host: n.Host, Name: "foreign", Kind: "cron", Spec: "0 * * * *"}); err != nil {
287 t.Fatal(err)
288 }
289 case "prelaunch":
290 target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
291 if err = f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
292 t.Fatal(err)
293 }
294 }
295 before, err := root.Reconcile.Registry.Get(n.Host)
296 if err != nil {
297 t.Fatal(err)
298 }
299 if err = root.servingHandover().Run(t.Context(), p, n); err == nil {
300 t.Fatal("accepted", attack)
301 }
302 after, err := root.Reconcile.Registry.Get(n.Host)
303 if err != nil || !reflect.DeepEqual(before, after) {
304 t.Fatal("refusal changed row", err)
305 }
306 source := f.db
307 if attack == "source-replaced" {
308 source = filepath.Join(filepath.Dir(f.db)+"-original", filepath.Base(f.db))
309 }
310 body, err := os.ReadFile(source)
311 if err != nil || string(body) != "served bytes" {
312 t.Fatal("refusal changed source", err)
313 }
314 })
315 }
316}
317
318// The stop coordinator writes the receipt used by the next handover. Process
319// and authentication observations remain fixtures; the rootrunner covers them.
320func TestServingStoppedWriterSecondHandover(t *testing.T) {
321 f := newServingRootFixture(t, true)
322 root := servingHandoverRoot(t, f)
323 f.f.peer = StopPeer{UID: 1001, PID: 77, ProcessStart: "edge-start", BootID: f.receipt.Target.BootID, ControllerID: "controller"}
324 coordinator := f.f.root(nil)
325 coordinator.Evidence, coordinator.Inventory = f.f.claims, root.Journals.Inventory
326 root.Stop.Coordinator = coordinator
327 root.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.f.peer, nil }
328 f.p.root.Stop.Coordinator = coordinator
329 socket := servingRootSocket(t, root)
330 transfer := coldHandoverWith(handoverRecoveryIdentity(t, f.actor), socket, func(context.Context, hostclaim.Observed) (hostguard.FenceReceipt, error) {
331 return f.receipt, nil
332 })
333 if err := transfer(t.Context(), f.predecessor, f.f.read()); err != nil {
334 t.Fatal(err)
335 }
336 target := recoveryTuple(f.f.read(), claimStopLocal{Actor: f.actor, BootID: f.receipt.Target.BootID, ControllerID: "controller"})
337 if err := f.f.journal.Prelaunch(t.Context(), hostbox.LaunchInventory{Target: target, ControllerPID: 77, ProcessStart: "edge-start", NoState: true, Groups: []hostbox.LaunchGroup{{RelativePath: "carlos/next", Device: 1, Inode: 99}}}); err != nil {
338 t.Fatal(err)
339 }
340 client := hostbox.ServingParkClient{Call: rootJournalClient(socket)}
341 first, err := client.Park(t.Context(), f.current, f.prior)
342 if err != nil {
343 t.Fatal(err)
344 }
345 f.tags(f.current.Stamp)
346 if err := client.Complete(t.Context(), first.ID, f.current); err != nil {
347 t.Fatal(err)
348 }
349 for suffix, bytes := range map[string]string{"": "second writer bytes", ".gen": f.current.Generation, "-wal": "second WAL", "-shm": "second SHM"} {
350 if err := os.WriteFile(f.db+suffix, []byte(bytes), 0600); err != nil {
351 t.Fatal(err)
352 }
353 }
354 r := hostbox.LaunchReservation{ID: "second:main", Role: "main", InvocationID: "second", Inputs: "digest", Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}, Executable: hostbox.LaunchFile{Device: 1, Inode: 3}}
355 if err := f.f.journal.IntendUnit(t.Context(), r); err != nil {
356 t.Fatal(err)
357 }
358 if err := f.f.journal.StartedUnit(t.Context(), r, hostbox.LaunchGroup{RelativePath: r.Group.RelativePath, Device: 1, Inode: 4, InvocationID: "second-unit"}, 99, "tenant-start"); err != nil {
359 t.Fatal(err)
360 }
361 o := f.f.read()
362 req := StopRequest{Region: f.actor.Region, Host: o.Host, Incarnation: f.current.Incarnation, InstallationStamp: f.current.Stamp, WriterSession: f.current.Session, Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: f.receipt.Target.BootID, ControllerID: "controller", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7", RosterRevision: 7}
363 drain := stopDrainFunc(func(_ context.Context, req StopRequest, fence string) (DrainAcknowledgement, error) {
364 return DrainClaimAcknowledgement(fence, req.ControllerID, "edge-start", nil), nil
365 })
366 if _, err := CollectClaimStop(t.Context(), socket, req, drain); err == nil {
367 t.Fatal("accepted an unsettled unit")
368 }
369 if err := f.f.journal.SettleUnit(t.Context(), target, r.ID); err != nil {
370 t.Fatal(err)
371 }
372 stopped, err := CollectClaimStop(t.Context(), socket, req, drain)
373 if err != nil {
374 t.Fatal("stop through transport", err)
375 }
376 f.receipt, err = f.f.guard.FenceReceiptFor(t.Context(), stopped.Target)
377 if err != nil || string(f.receipt.Bytes) != string(stopped.Bytes) {
378 t.Fatal("durable receipt", err)
379 }
380 previous := f.f.read()
381 h := previous.Claim.Holder
382 admitted, err := hostclaim.Update(t.Context(), f.f.claims, previous, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: f.actor.Box, Region: f.actor.Region, PredecessorRegion: f.actor.Region, Mode: "cold", NewGeneration: "G3"}))
383 if err != nil {
384 t.Fatal(err)
385 }
386 prior := f.current
387 f.current, err = servingWriterIdentity(admitted, f.actor.Box, f.actor.Region)
388 if err != nil {
389 t.Fatal(err)
390 }
391 if err := transfer(t.Context(), previous, admitted); err != nil {
392 t.Fatal("second handover", err)
393 }
394 parked, err := client.Park(t.Context(), f.current, prior)
395 if err != nil {
396 t.Fatal("second park", err)
397 }
398 f.tags(f.current.Stamp)
399 if err := client.Complete(t.Context(), parked.ID, f.current); err != nil {
400 t.Fatal(err)
401 }
402 record, err := f.p.ledger.read(parked.ID)
403 if err != nil || !record.Complete || record.Disposition != servingParkRetain || len(record.Receipts) != 1 || string(record.Receipts[0].Bytes) != string(stopped.Bytes) {
404 t.Fatal("second park did not consume exact transport stop receipt", err)
405 }
406 old, err := f.p.ledger.read(first.ID)
407 if err != nil || !old.Closed || old.Cause != "replacement" {
408 t.Fatal("previous park was not replaced", err)
409 }
410 for suffix, want := range map[string]string{"": "second writer bytes", ".gen": prior.Generation, "-wal": "second WAL", "-shm": "second SHM"} {
411 body, err := os.ReadFile(filepath.Join(f.f.guard.DataDir, "instances", ".serving-parked", parked.ID, filepath.Base(f.db)+suffix))
412 if err != nil || string(body) != want {
413 t.Fatal("second park changed bytes", suffix, err)
414 }
415 }
416 if replay, err := client.Park(t.Context(), f.current, prior); err != nil || replay != parked {
417 t.Fatal("second park replay", err)
418 }
419 if after, err := f.p.ledger.read(parked.ID); err != nil || !reflect.DeepEqual(record, after) {
420 t.Fatal("second park replay changed record", err)
421 }
422 servingRestoreAfterStoppedWriter(t, f, client)
423 if after, err := f.p.ledger.read(parked.ID); err != nil || !reflect.DeepEqual(record, after) {
424 t.Fatal("restore changed retained park", err)
425 }
426}
427
428// This connects production ColdRestore and retirement enrollment to the stopped
429// writer's park. The exact-version LTX baseline and local policy are fixtures;
430// full wake preparation and C8 remain separate acceptance gates.
431func servingRestoreAfterStoppedWriter(t *testing.T, f *servingRootFixture, parker servingDatabaseParker) {
432 t.Helper()
433 plan := plan12RestoreBaseline(t, f.f)
434 before, err := f.f.data.GetVersion(t.Context(), f.f.bucket, plan.Files[0].Key, plan.Files[0].VersionID)
435 if err != nil {
436 t.Fatal(err)
437 }
438 admitted := f.f.read()
439 in := ClaimLaunchInputs{DBPath: f.db}
440 r := &claimServingArtifactRetirement{Claims: f.f.claims, Guard: f.f.guard, DataDir: f.f.guard.DataDir, Box: f.actor.Box, Region: f.actor.Region, Root: parker}
441 source := hostbox.WorkSource{Guard: f.f.guard, Box: f.actor.Box, Region: f.actor.Region, Now: func() time.Time { return f.f.now }, Policy: func(_ context.Context, c *hostclaim.Claim, selected hostbox.Selection, _ hostwrite.Identity) error {
442 return hostbox.Serving(c, selected)
443 }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }}
444 work, err := source.Select(t.Context(), admitted.Host, "restore")
445 if err != nil {
446 t.Fatal("select restore work", err)
447 }
448 if err := r.Recover(t.Context(), admitted, in); err != nil {
449 t.Fatal("recover completed retirement", err)
450 }
451 enrolled := 0
452 restore := hostbox.ColdRestore{Reader: f.f.data, Bucket: f.f.bucket, Generation: f.current.Generation, Work: work,
453 BeforeLegacyInstall: func(ctx context.Context, _, _ string) error { return r.BeforeLegacyInstall(ctx, admitted, in, work) },
454 BeforeInstallLocked: func(ctx context.Context, _, _ string) error {
455 if err := r.EnrollLocked(ctx, admitted, in); err != nil {
456 return err
457 }
458 enrolled++
459 return nil
460 },
461 RetireArtifacts: func(ctx context.Context, generation string) error { return r.Retire(ctx, admitted, in, generation) },
462 }
463 if err := restore.Install(t.Context(), f.db, plan); err != nil {
464 t.Fatal("restore after stopped writer", err)
465 }
466 if enrolled != 1 || mwReadDB(t, f.db) != "selected recovery baseline" {
467 t.Fatal("restore missed enrollment or restored the wrong baseline")
468 }
469 if marker, err := hostbox.ReadGenerationMarker(f.db + ".gen"); err != nil || marker != f.current.Generation {
470 t.Fatal("restored generation", marker, err)
471 }
472 after, err := f.f.data.GetVersion(t.Context(), f.f.bucket, plan.Files[0].Key, plan.Files[0].VersionID)
473 if err != nil || string(after) != string(before) {
474 t.Fatal("restore changed remote baseline", err)
475 }
476}
477