//go:build linux package host import ( "context" "encoding/json" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "os" "path/filepath" "reflect" "testing" ) func TestPlan14ServingHandoverPins(t *testing.T) { f := newServingRootFixture(t, true) root := servingHandoverRoot(t, f) f.mutateRoot(func(m map[string]any) { m["Installation"].(map[string]any)["Artifacts"] = map[string]any{"Schema": 1, "StateDirectory": true} }) if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" { root.Reconcile.Retirement.JournalDir = claimRetireJournalDir("", "") } readHandovers := func() ([]hostbox.GenerationHandover, error) { if os.Getenv("PLAN14_R11_HANDOVER_ROOT") == "1" { return productionGenerationHandovers(t.Context()) } return generationReadHandovers(t.Context(), &rollbackDisk{dir: filepath.Join(root.Reconcile.Retirement.JournalDir, "serving")}) } 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} if err := root.servingHandover().Run(t.Context(), f.receipt.Target, next); err != nil { t.Fatal(err) } installs, err := f.f.guard.Root.GenerationInstallations(t.Context()) if err != nil { t.Fatal(err) } payloads, err := f.f.journal.GenerationPayloads(t.Context()) if err != nil { t.Fatal(err) } snapshot := generationInventory{Installations: installs, Payloads: payloads} snapshot.Handovers, err = readHandovers() if err != nil { t.Fatal(err) } if _, err = generationTransfers(t.Context(), snapshot); err != nil { t.Fatal("transfers", err) } if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { t.Fatal("registry after actual serving handover", err) } physical, err := generationPhysicalPayloads(t.Context(), snapshot) if err != nil { t.Fatal(err) } if physical[0].Inventory.Entries[0].Identity != physical[1].Inventory.Entries[0].Identity { t.Fatal("predecessor did not follow verified persistent identity") } t.Run("recreated-namespace", func(t *testing.T) { raw, _ := json.Marshal(snapshot) var recreated generationInventory if err := json.Unmarshal(raw, &recreated); err != nil { t.Fatal(err) } for i, e := range recreated.Payloads[0].Inventory.Entries { if e.Kind != "path" { continue } current := recreated.Payloads[1].Inventory.Entries[i].Identity old := current old.Mount += 100 recreated.Payloads[0].Inventory.Entries[i].Identity = old recreated.Handovers[0].Inventory.Entries[i].Identity = old } if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, recreated); err != nil { t.Fatal("recreated namespace transfer", err) } if _, err := generationPhysicalPayloads(t.Context(), recreated); err != nil { t.Fatal("recreated namespace payloads", err) } }) original, _ := json.Marshal(snapshot) for _, attack := range []string{"missing journal", "prepared", "successor", "barrier", "receipt", "retired", "path", "row", "revision", "duplicate", "cycle", "old boot"} { t.Run(attack, func(t *testing.T) { var bad generationInventory if err := json.Unmarshal(original, &bad); err != nil { t.Fatal(err) } switch attack { case "missing journal": bad.Handovers = nil case "prepared": bad.Handovers[0].Phase = "prepared" case "successor": bad.Handovers[0].Successor.Stamp++ case "barrier": bad.Handovers[0].Barrier.ControllerID = "other" case "receipt": bad.Handovers[0].Receipt.Bytes = []byte("other") case "retired": bad.Installations[0].Retired = nil case "path": bad.Payloads[1].Inventory.Entries[0].Inode++ case "row": bad.Payloads[1].Inventory.Entries[1].RowIdentity = "other" case "revision": bad.Payloads[0].Revision++ case "duplicate": bad.Handovers = append(bad.Handovers, bad.Handovers[0]) case "cycle": bad.Handovers[0].Successor.Stamp = bad.Handovers[0].Prior.Stamp case "old boot": bad.Payloads[1].Inventory.Entries[0].Identity.Boot = "other-boot" } if _, err := generationRegistryInventory(t.Context(), root.Reconcile.Registry, bad); err == nil { t.Fatal("accepted", attack) } }) } before := f.f.read() receipt := servingStop(t, f.f, before, f.receipt.Target.BootID) 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"})) if err != nil { t.Fatal(err) } next.ETag, next.Generation, next.Stamp, next.Session = admitted.ETag, admitted.Claim.Holder.Generation, admitted.Claim.Holder.Stamp, admitted.Claim.Holder.Session if err = root.servingHandover().Run(t.Context(), receipt.Target, next); err != nil { t.Fatal("second transfer", err) } snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context()) if err != nil { t.Fatal(err) } snapshot.Payloads, err = f.f.journal.GenerationPayloads(t.Context()) if err != nil { t.Fatal(err) } snapshot.Handovers, err = readHandovers() if err != nil { t.Fatal(err) } if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { t.Fatal("second transfer registry", err) } if len(snapshot.Handovers) != 2 || len(snapshot.Payloads) != 3 { t.Fatal("transfer lost retained authority") } physical, err = generationPhysicalPayloads(t.Context(), snapshot) if err != nil { t.Fatal(err) } for i := range physical { if !reflect.DeepEqual(physical[i].Inventory.Entries[0].Identity, physical[2].Inventory.Entries[0].Identity) { t.Fatal("chain identity", i) } } // The initial installation and stop evidence are fixtures. Transfers, root // retirement, row removal and directory parking below use their production adapters. stopped := servingStop(t, f.f, f.f.read(), f.receipt.Target.BootID) terminal := snapshot.Payloads[2].Inventory inventory := hostbox.RetirementInventory{Host: terminal.Host, Captured: hostguard.Captured{Incarnation: terminal.Incarnation, Stamp: terminal.InstallationStamp}, Ownership: terminal.Entries[0].Ownership} for _, e := range terminal.Entries { 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}) } retirement, err := hostbox.NewRetirementJournal(stopped.Target.FenceID, inventory) if err != nil { t.Fatal(err) } retirement.Target, retirement.StopReceipt = &stopped.Target, stopped.Bytes data := f.f.guard.DataDir t.Setenv("CARLOS_DATA_DIR", data) if err = f.f.claims.Delete(t.Context(), snapshot.Installations[0].Installation.Record); err != nil { t.Fatal(err) } parker := hostbox.TypedParker{Roots: map[string]string{"data": data}, ParkedRoots: map[string]string{"data": data}, Incarnation: terminal.Incarnation, Host: terminal.Host, Retirement: retirement.ID} 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 }} if err = runner.Run(t.Context(), stopped.Target, retirement); err != nil { t.Fatal("terminal retirement", err) } raw, err := os.ReadFile(filepath.Join(runner.JournalDir, retirement.ID+".json")) if err != nil { t.Fatal(err) } if err = json.Unmarshal(raw, &retirement); err != nil || !retirement.Complete { t.Fatal(err) } snapshot.Retirements = []hostbox.RetirementJournal{retirement} snapshot.Installations, err = f.f.guard.Root.GenerationInstallations(t.Context()) if err != nil { t.Fatal(err) } if _, err = generationRequiredInventory(t.Context(), f.f.claims, snapshot, nil); err != nil { t.Fatal("deleted record through transfer chain", err) } if _, err = generationRegistryInventory(t.Context(), root.Reconcile.Registry, snapshot); err != nil { t.Fatal("retired transfer chain", err) } physical, err = generationPhysicalPayloads(t.Context(), snapshot) if err != nil { t.Fatal(err) } for _, payload := range physical { for _, e := range payload.Inventory.Entries { if e.Kind == "path" { if _, err = os.Stat(filepath.Join(data, e.RelativePath)); err != nil { t.Fatal("retained predecessor location", err) } } } } }