//go:build linux package host import ( "bytes" "context" "crypto/ed25519" "errors" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // S1: the migration census reports a legacy route row as the box's route // installation of its frozen record, so backfill-adopt can give the host // to this box. Every other row shape refuses the census. func TestRootMigrationCensusReportsLegacyRoutes(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{}} records := map[string]hostops.Record{} for _, r := range leoRecords(t) { if _, err := store.Put(t.Context(), r.Holder.Key, r.Body, ""); err != nil { t.Fatal(err) } r, err = hostops.ReadRecord(t.Context(), store, r.Holder.Key) if err != nil { t.Fatal(err) } records[r.Holder.Host] = r plan.Hosts[r.Holder.Host] = hostops.BackfillHost{Key: r.Holder.Key, ETag: r.ETag, Acquisition: r.Holder.Acquisition} } 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) } dataDir := t.TempDir() var rows []registry.Route 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 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 }, routes: func() ([]registry.Route, error) { return rows, nil }, dataDir: dataDir} rows = []registry.Route{leoRoute(dataDir)} s, err := c.collect(t.Context()) if err != nil { t.Fatal(err) } if err := s.preflight(c.identity); err != nil { t.Fatal(err) } inc, _ := records[leoClaimtrial].Incarnation() want := hostops.CensusInstallation{Host: leoClaimtrial, Account: "bab", App: "claimtrial", Family: "instance", Key: records[leoClaimtrial].Holder.Key, Incarnation: inc, Route: true} if len(s.Report.Installations) != 1 || s.Report.Installations[0] != want || len(s.Items) != 0 { t.Fatalf("report = %+v, items %+v", s.Report.Installations, s.Items) } if err := hostops.PublishCensusReport(t.Context(), store, c.directory, s.Report, owners.EvidenceKey); err != nil { t.Fatal(err) } // No registry listing reports nothing, as before. without := c without.routes = nil if s, err := without.collect(t.Context()); err != nil || len(s.Report.Installations) != 0 { t.Fatalf("no listing = %+v, %v", s.Report.Installations, err) } for name, mutate := range map[string]func(*registry.Route){ "unit": func(r *registry.Route) { r.Unit = "app.service" }, "backing": func(r *registry.Route) { r.Backing = "unit" }, "replica": func(r *registry.Route) { r.Replica = true }, "seed": func(r *registry.Route) { r.Seed = "seed" }, "tunnel": func(r *registry.Route) { r.Tunnel = "t" }, "sidecar": func(r *registry.Route) { r.Kind = "sidecar" }, "service": func(r *registry.Route) { r.Kind = "service" }, "flat layout": func(r *registry.Route) { r.DBPath = filepath.Join(dataDir, r.Host+".db") }, "claimed row": func(r *registry.Route) { r.ClaimIncarnation, r.ClaimStamp, r.ClaimOwner, r.ClaimRowID = "inc", 1, "owner", "row" }, "another app": func(r *registry.Route) { r.App = "wakecheck" }, "another account": func(r *registry.Route) { r.Account = "zzz" }, "no version": func(r *registry.Route) { r.Version = "" }, "no record": func(r *registry.Route) { r.Host, r.DBPath = "other.example.com", filepath.Join(dataDir, "instances", "other.example.com", "other.example.com.db") }, } { t.Run(name, func(t *testing.T) { row := leoRoute(dataDir) mutate(&row) rows = []registry.Route{row} if _, err := c.collect(t.Context()); !errors.Is(err, errRootMigrationBusy) { t.Fatalf("collect = %v, want the census refused", err) } }) } } // The dev trial deletes every instance record before the window, so the // collecting plan freezes zero hosts ("hosts":{}). The migration census // accepts it on a box with no route rows, reports zero installations, and // the signer's backfill-done closes it over that report. func TestRootMigrationCensusAcceptsAZeroHostPlan(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)) signer := release.RootSigner(key) directory := owners.RetirementAuthority.Directory roster, err := directory.Read(t.Context()) if err != nil { t.Fatal(err) } plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: roster.Roster.Revision, CreatedAt: time.Now().Add(-time.Minute), Hosts: map[string]hostops.BackfillHost{}} raw, err := release.SignRootEnvelope(signer, plan) if err != nil { t.Fatal(err) } tag, err := store.Put(t.Context(), hostops.BackfillKey, raw, "") if err != nil { t.Fatal(err) } c := rootMigrationCollector{store: store, directory: directory, identity: rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: 1}, seeds: &migrationSeeds{}, 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 }, routes: func() ([]registry.Route, error) { return nil, nil }, dataDir: t.TempDir()} s, err := c.collect(t.Context()) if err != nil { t.Fatal("collect over a zero-host plan:", err) } if err := s.preflight(c.identity); err != nil { t.Fatal(err) } if s.Report.Installations == nil || len(s.Report.Installations) != 0 || len(s.Records) != 0 || len(s.Items) != 0 { t.Fatalf("zero-host census = %+v", s) } if err := hostops.PublishCensusReport(t.Context(), store, directory, s.Report, owners.EvidenceKey); err != nil { t.Fatal(err) } b := &hostops.Backfill{Store: store, Directory: directory} unreported, err := b.Done(t.Context(), signer, tag) if err != nil { t.Fatal("done over the zero-host census:", err) } if err := hostops.CheckCreateFreeze(t.Context(), store, directory.PublicKey); err != nil { t.Fatal(err) } t.Logf("done over zero hosts; unreported %v", unreported) }