package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) type migrationSeeds struct { journal []string routes []registry.Route } func (s *migrationSeeds) ImportJournalHosts() ([]string, error) { return s.journal, nil } func (s *migrationSeeds) List() ([]registry.Route, error) { return s.routes, nil } func TestPlan14RootMigrationCensusSeeds(t *testing.T) { store, _, owners, _ := fix4JournalFixture(t) actor, err := owners.Identity.Verify(t.Context()) if err != nil { t.Fatal(err) } key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: 1, CreatedAt: time.Now(), Hosts: map[string]hostops.BackfillHost{}} raw, err := release.SignRootEnvelope(release.RootSigner(key), plan) if err != nil { t.Fatal(err) } if _, err := store.Put(t.Context(), hostops.BackfillKey, raw, ""); err != nil { t.Fatal(err) } seeds := &migrationSeeds{} c := rootMigrationCollector{store: store, directory: owners.RetirementAuthority.Directory, identity: rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: 1}, seeds: seeds, installations: func(context.Context) ([]hostguard.FenceView, error) { return nil, nil }, launches: func(context.Context) ([]hostbox.LaunchInventory, error) { return nil, nil }, reservations: func(context.Context) ([]release.GenerationReservation, error) { return nil, nil }, boot: func() (string, error) { return "boot", nil }} s, err := c.collect(t.Context()) if err != nil { t.Fatal(err) } if err := s.preflight(c.identity); err != nil { t.Fatal(err) } if s.Report.Seeds == nil || s.Report.Seeds.Journaled == nil || s.Report.Seeds.Gated == nil { t.Fatal("missing seed census", s.Report) } if err := hostops.PublishCensusReport(t.Context(), store, c.directory, s.Report, owners.EvidenceKey); err != nil { t.Fatal(err) } keys, err := store.List(t.Context(), "control/host-census/") if err != nil || len(keys) != 1 { t.Fatal(keys, err) } body, _, err := store.Get(t.Context(), keys[0]) if err != nil { t.Fatal(err) } var envelope hostops.CensusEnvelope if json.Unmarshal(body, &envelope) != nil || envelope.Report.Seeds == nil { t.Fatal("not a real signed seeds report") } for _, kind := range []string{"journal", "gate", "unknown-launch", "reservation"} { t.Run(kind, func(t *testing.T) { cc := c defer func() { seeds.journal = nil; seeds.routes = nil }() switch kind { case "journal": seeds.journal = []string{"a.example"} case "gate": seeds.routes = []registry.Route{{Host: "a.example", ImportGate: "closed"}} case "unknown-launch": cc.launches = func(context.Context) ([]hostbox.LaunchInventory, error) { return []hostbox.LaunchInventory{{Target: hostguard.FenceTarget{Host: "unknown.example", Op: "move"}}}, nil } case "reservation": cc.reservations = func(context.Context) ([]release.GenerationReservation, error) { return []release.GenerationReservation{{}}, nil } } s, err := cc.collect(t.Context()) if err == nil { err = s.preflight(c.identity) } if err == nil { t.Fatal("unsafe census accepted") } }) } } // Census condition 1 (fix round 2): journal-migrate's collector refuses a // settled installation with ErrFenced, as MigrationInstallations refused // every quarantined record before settlement existed. The installation is // one the claim still names on this box, so without the refusal it would // enter the migration snapshot as a live, local item. The control is the // same view, not settled: it does enter the snapshot. func TestPlan14RootMigrationCollectorRefusesSettled(t *testing.T) { ctx := t.Context() store, _, owners, _ := fix4JournalFixture(t) actor, err := owners.Identity.Verify(ctx) if err != nil { t.Fatal(err) } r := leoRecords(t)[0] var body struct { Region string `json:"region"` CreatedAt time.Time `json:"created_at"` } if err := json.Unmarshal(r.Body, &body); err != nil { t.Fatal(err) } if _, err := store.Put(ctx, r.Holder.Key, r.Body, ""); err != nil { t.Fatal(err) } absent, err := hostclaim.Read(ctx, store, r.Holder.Host) if err != nil { t.Fatal(err) } o, err := hostclaim.Update(ctx, store, absent, hostclaim.BeginCreate(hostclaim.NewHolder{Family: "instance", Key: r.Holder.Key, Account: r.Holder.Account, App: r.Holder.App, Host: r.Holder.Host, Region: "ie", RecordRegion: body.Region, CreatedAt: body.CreatedAt}, r.Body, "create", "console", time.Now())) if err != nil { t.Fatal(err) } if o, err = hostclaim.Update(ctx, store, o, hostclaim.CompleteCreate("create", actor.Box)); err != nil { t.Fatal(err) } // TEST-ONLY transition: a writer session on this box. if o, err = hostclaim.Update(ctx, store, o, func(c *hostclaim.Claim) error { c.Holder.Session, c.Holder.WriterBox = 1, actor.Box; return nil }); err != nil { t.Fatal(err) } r, err = hostops.ReadRecord(ctx, store, r.Holder.Key) if err != nil { t.Fatal(err) } plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: 1, CreatedAt: time.Now(), Hosts: map[string]hostops.BackfillHost{r.Holder.Host: {Key: r.Holder.Key, ETag: r.ETag, Acquisition: r.Holder.Acquisition}}} raw, err := release.SignRootEnvelope(release.RootSigner(ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize))), plan) if err != nil { t.Fatal(err) } if _, err := store.Put(ctx, hostops.BackfillKey, raw, ""); err != nil { t.Fatal(err) } h := o.Claim.Holder view := hostguard.FenceView{Schema: 1, Host: o.Host, Captured: hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, Installation: hostguard.RootInstallation{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: actor.Box, Enrollment: actor.Enrollment, Migration: 1, InventoryRevision: 2}} for _, settled := range []bool{false, true} { v := view v.Settled = settled c := rootMigrationCollector{store: store, directory: owners.RetirementAuthority.Directory, identity: rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: 1}, seeds: &migrationSeeds{}, installations: func(context.Context) ([]hostguard.FenceView, error) { return []hostguard.FenceView{v}, nil }, launches: func(context.Context) ([]hostbox.LaunchInventory, error) { return nil, nil }, reservations: func(context.Context) ([]release.GenerationReservation, error) { return nil, nil }, boot: func() (string, error) { return "boot", nil }} s, err := c.collect(ctx) if settled { if !errors.Is(err, hostguard.ErrFenced) { t.Fatalf("collector admitted a settled installation: %v, items %+v", err, s.Items) } continue } if err != nil || len(s.Items) != 1 || s.Items[0].Installation != view.Installation { t.Fatalf("control: the unsettled view: %v, items %+v", err, s.Items) } } }