| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "crypto/ed25519" |
| 9 | "errors" |
| 10 | "path/filepath" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/hostbox" |
| 15 | "github.com/carlosframework/platform/internal/hostguard" |
| 16 | "github.com/carlosframework/platform/internal/hostops" |
| 17 | "github.com/carlosframework/platform/internal/registry" |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | ) |
| 20 | |
| 21 | // S1: the migration census reports a legacy route row as the box's route |
| 22 | // installation of its frozen record, so backfill-adopt can give the host |
| 23 | // to this box. Every other row shape refuses the census. |
| 24 | func TestRootMigrationCensusReportsLegacyRoutes(t *testing.T) { |
| 25 | store, _, owners, _ := fix4JournalFixture(t) |
| 26 | actor, err := owners.Identity.Verify(t.Context()) |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| 31 | plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: 1, CreatedAt: time.Now(), Hosts: map[string]hostops.BackfillHost{}} |
| 32 | records := map[string]hostops.Record{} |
| 33 | for _, r := range leoRecords(t) { |
| 34 | if _, err := store.Put(t.Context(), r.Holder.Key, r.Body, ""); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | r, err = hostops.ReadRecord(t.Context(), store, r.Holder.Key) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | records[r.Holder.Host] = r |
| 42 | plan.Hosts[r.Holder.Host] = hostops.BackfillHost{Key: r.Holder.Key, ETag: r.ETag, Acquisition: r.Holder.Acquisition} |
| 43 | } |
| 44 | raw, err := release.SignRootEnvelope(release.RootSigner(key), plan) |
| 45 | if err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | if _, err := store.Put(t.Context(), hostops.BackfillKey, raw, ""); err != nil { |
| 49 | t.Fatal(err) |
| 50 | } |
| 51 | dataDir := t.TempDir() |
| 52 | var rows []registry.Route |
| 53 | c := rootMigrationCollector{store: store, directory: owners.RetirementAuthority.Directory, identity: rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: 1}, |
| 54 | seeds: &migrationSeeds{}, installations: func(context.Context) ([]hostguard.FenceView, error) { return nil, nil }, launches: func(context.Context) ([]hostbox.LaunchInventory, error) { return nil, nil }, |
| 55 | reservations: func(context.Context) ([]release.GenerationReservation, error) { return nil, nil }, boot: func() (string, error) { return "boot", nil }, |
| 56 | routes: func() ([]registry.Route, error) { return rows, nil }, dataDir: dataDir} |
| 57 | rows = []registry.Route{leoRoute(dataDir)} |
| 58 | s, err := c.collect(t.Context()) |
| 59 | if err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | if err := s.preflight(c.identity); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | inc, _ := records[leoClaimtrial].Incarnation() |
| 66 | want := hostops.CensusInstallation{Host: leoClaimtrial, Account: "bab", App: "claimtrial", Family: "instance", Key: records[leoClaimtrial].Holder.Key, Incarnation: inc, Route: true} |
| 67 | if len(s.Report.Installations) != 1 || s.Report.Installations[0] != want || len(s.Items) != 0 { |
| 68 | t.Fatalf("report = %+v, items %+v", s.Report.Installations, s.Items) |
| 69 | } |
| 70 | if err := hostops.PublishCensusReport(t.Context(), store, c.directory, s.Report, owners.EvidenceKey); err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | // No registry listing reports nothing, as before. |
| 74 | without := c |
| 75 | without.routes = nil |
| 76 | if s, err := without.collect(t.Context()); err != nil || len(s.Report.Installations) != 0 { |
| 77 | t.Fatalf("no listing = %+v, %v", s.Report.Installations, err) |
| 78 | } |
| 79 | for name, mutate := range map[string]func(*registry.Route){ |
| 80 | "unit": func(r *registry.Route) { r.Unit = "app.service" }, |
| 81 | "backing": func(r *registry.Route) { r.Backing = "unit" }, |
| 82 | "replica": func(r *registry.Route) { r.Replica = true }, |
| 83 | "seed": func(r *registry.Route) { r.Seed = "seed" }, |
| 84 | "tunnel": func(r *registry.Route) { r.Tunnel = "t" }, |
| 85 | "sidecar": func(r *registry.Route) { r.Kind = "sidecar" }, |
| 86 | "service": func(r *registry.Route) { r.Kind = "service" }, |
| 87 | "flat layout": func(r *registry.Route) { r.DBPath = filepath.Join(dataDir, r.Host+".db") }, |
| 88 | "claimed row": func(r *registry.Route) { |
| 89 | r.ClaimIncarnation, r.ClaimStamp, r.ClaimOwner, r.ClaimRowID = "inc", 1, "owner", "row" |
| 90 | }, |
| 91 | "another app": func(r *registry.Route) { r.App = "wakecheck" }, |
| 92 | "another account": func(r *registry.Route) { r.Account = "zzz" }, |
| 93 | "no version": func(r *registry.Route) { r.Version = "" }, |
| 94 | "no record": func(r *registry.Route) { |
| 95 | r.Host, r.DBPath = "other.example.com", filepath.Join(dataDir, "instances", "other.example.com", "other.example.com.db") |
| 96 | }, |
| 97 | } { |
| 98 | t.Run(name, func(t *testing.T) { |
| 99 | row := leoRoute(dataDir) |
| 100 | mutate(&row) |
| 101 | rows = []registry.Route{row} |
| 102 | if _, err := c.collect(t.Context()); !errors.Is(err, errRootMigrationBusy) { |
| 103 | t.Fatalf("collect = %v, want the census refused", err) |
| 104 | } |
| 105 | }) |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | // The dev trial deletes every instance record before the window, so the |
| 110 | // collecting plan freezes zero hosts ("hosts":{}). The migration census |
| 111 | // accepts it on a box with no route rows, reports zero installations, and |
| 112 | // the signer's backfill-done closes it over that report. |
| 113 | func TestRootMigrationCensusAcceptsAZeroHostPlan(t *testing.T) { |
| 114 | store, _, owners, _ := fix4JournalFixture(t) |
| 115 | actor, err := owners.Identity.Verify(t.Context()) |
| 116 | if err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| 120 | signer := release.RootSigner(key) |
| 121 | directory := owners.RetirementAuthority.Directory |
| 122 | roster, err := directory.Read(t.Context()) |
| 123 | if err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: roster.Roster.Revision, CreatedAt: time.Now().Add(-time.Minute), Hosts: map[string]hostops.BackfillHost{}} |
| 127 | raw, err := release.SignRootEnvelope(signer, plan) |
| 128 | if err != nil { |
| 129 | t.Fatal(err) |
| 130 | } |
| 131 | tag, err := store.Put(t.Context(), hostops.BackfillKey, raw, "") |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | c := rootMigrationCollector{store: store, directory: directory, identity: rootMigrationIdentity{Deployment: actor.ControlBucket, Box: actor.Box, Enrollment: actor.Enrollment, Generation: 1}, |
| 136 | seeds: &migrationSeeds{}, installations: func(context.Context) ([]hostguard.FenceView, error) { return nil, nil }, launches: func(context.Context) ([]hostbox.LaunchInventory, error) { return nil, nil }, |
| 137 | reservations: func(context.Context) ([]release.GenerationReservation, error) { return nil, nil }, boot: func() (string, error) { return "boot", nil }, |
| 138 | routes: func() ([]registry.Route, error) { return nil, nil }, dataDir: t.TempDir()} |
| 139 | s, err := c.collect(t.Context()) |
| 140 | if err != nil { |
| 141 | t.Fatal("collect over a zero-host plan:", err) |
| 142 | } |
| 143 | if err := s.preflight(c.identity); err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | if s.Report.Installations == nil || len(s.Report.Installations) != 0 || len(s.Records) != 0 || len(s.Items) != 0 { |
| 147 | t.Fatalf("zero-host census = %+v", s) |
| 148 | } |
| 149 | if err := hostops.PublishCensusReport(t.Context(), store, directory, s.Report, owners.EvidenceKey); err != nil { |
| 150 | t.Fatal(err) |
| 151 | } |
| 152 | b := &hostops.Backfill{Store: store, Directory: directory} |
| 153 | unreported, err := b.Done(t.Context(), signer, tag) |
| 154 | if err != nil { |
| 155 | t.Fatal("done over the zero-host census:", err) |
| 156 | } |
| 157 | if err := hostops.CheckCreateFreeze(t.Context(), store, directory.PublicKey); err != nil { |
| 158 | t.Fatal(err) |
| 159 | } |
| 160 | t.Logf("done over zero hosts; unreported %v", unreported) |
| 161 | } |
| 162 | |