| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostguard" |
| 15 | "github.com/carlosframework/platform/internal/hostops" |
| 16 | "github.com/carlosframework/platform/internal/registry" |
| 17 | "github.com/carlosframework/platform/internal/release" |
| 18 | ) |
| 19 | |
| 20 | type migrationSeeds struct { |
| 21 | journal []string |
| 22 | routes []registry.Route |
| 23 | } |
| 24 | |
| 25 | func (s *migrationSeeds) ImportJournalHosts() ([]string, error) { return s.journal, nil } |
| 26 | func (s *migrationSeeds) List() ([]registry.Route, error) { return s.routes, nil } |
| 27 | |
| 28 | func TestPlan14RootMigrationCensusSeeds(t *testing.T) { |
| 29 | store, _, owners, _ := fix4JournalFixture(t) |
| 30 | actor, err := owners.Identity.Verify(t.Context()) |
| 31 | if err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| 35 | plan := hostops.BackfillPlan{V: 1, ID: "migration", Phase: "collecting", DirectoryRevision: 1, CreatedAt: time.Now(), Hosts: map[string]hostops.BackfillHost{}} |
| 36 | raw, err := release.SignRootEnvelope(release.RootSigner(key), plan) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | if _, err := store.Put(t.Context(), hostops.BackfillKey, raw, ""); err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | seeds := &migrationSeeds{} |
| 44 | 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 }} |
| 45 | s, err := c.collect(t.Context()) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | if err := s.preflight(c.identity); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | if s.Report.Seeds == nil || s.Report.Seeds.Journaled == nil || s.Report.Seeds.Gated == nil { |
| 53 | t.Fatal("missing seed census", s.Report) |
| 54 | } |
| 55 | if err := hostops.PublishCensusReport(t.Context(), store, c.directory, s.Report, owners.EvidenceKey); err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | keys, err := store.List(t.Context(), "control/host-census/") |
| 59 | if err != nil || len(keys) != 1 { |
| 60 | t.Fatal(keys, err) |
| 61 | } |
| 62 | body, _, err := store.Get(t.Context(), keys[0]) |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | var envelope hostops.CensusEnvelope |
| 67 | if json.Unmarshal(body, &envelope) != nil || envelope.Report.Seeds == nil { |
| 68 | t.Fatal("not a real signed seeds report") |
| 69 | } |
| 70 | for _, kind := range []string{"journal", "gate", "unknown-launch", "reservation"} { |
| 71 | t.Run(kind, func(t *testing.T) { |
| 72 | cc := c |
| 73 | defer func() { seeds.journal = nil; seeds.routes = nil }() |
| 74 | switch kind { |
| 75 | case "journal": |
| 76 | seeds.journal = []string{"a.example"} |
| 77 | case "gate": |
| 78 | seeds.routes = []registry.Route{{Host: "a.example", ImportGate: "closed"}} |
| 79 | case "unknown-launch": |
| 80 | cc.launches = func(context.Context) ([]hostbox.LaunchInventory, error) { |
| 81 | return []hostbox.LaunchInventory{{Target: hostguard.FenceTarget{Host: "unknown.example", Op: "move"}}}, nil |
| 82 | } |
| 83 | case "reservation": |
| 84 | cc.reservations = func(context.Context) ([]release.GenerationReservation, error) { |
| 85 | return []release.GenerationReservation{{}}, nil |
| 86 | } |
| 87 | } |
| 88 | s, err := cc.collect(t.Context()) |
| 89 | if err == nil { |
| 90 | err = s.preflight(c.identity) |
| 91 | } |
| 92 | if err == nil { |
| 93 | t.Fatal("unsafe census accepted") |
| 94 | } |
| 95 | }) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | // Census condition 1 (fix round 2): journal-migrate's collector refuses a |
| 100 | // settled installation with ErrFenced, as MigrationInstallations refused |
| 101 | // every quarantined record before settlement existed. The installation is |
| 102 | // one the claim still names on this box, so without the refusal it would |
| 103 | // enter the migration snapshot as a live, local item. The control is the |
| 104 | // same view, not settled: it does enter the snapshot. |
| 105 | func TestPlan14RootMigrationCollectorRefusesSettled(t *testing.T) { |
| 106 | ctx := t.Context() |
| 107 | store, _, owners, _ := fix4JournalFixture(t) |
| 108 | actor, err := owners.Identity.Verify(ctx) |
| 109 | if err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | r := leoRecords(t)[0] |
| 113 | var body struct { |
| 114 | Region string `json:"region"` |
| 115 | CreatedAt time.Time `json:"created_at"` |
| 116 | } |
| 117 | if err := json.Unmarshal(r.Body, &body); err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if _, err := store.Put(ctx, r.Holder.Key, r.Body, ""); err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | absent, err := hostclaim.Read(ctx, store, r.Holder.Host) |
| 124 | if err != nil { |
| 125 | t.Fatal(err) |
| 126 | } |
| 127 | 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())) |
| 128 | if err != nil { |
| 129 | t.Fatal(err) |
| 130 | } |
| 131 | if o, err = hostclaim.Update(ctx, store, o, hostclaim.CompleteCreate("create", actor.Box)); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | // TEST-ONLY transition: a writer session on this box. |
| 135 | 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 { |
| 136 | t.Fatal(err) |
| 137 | } |
| 138 | r, err = hostops.ReadRecord(ctx, store, r.Holder.Key) |
| 139 | if err != nil { |
| 140 | t.Fatal(err) |
| 141 | } |
| 142 | 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}}} |
| 143 | raw, err := release.SignRootEnvelope(release.RootSigner(ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize))), plan) |
| 144 | if err != nil { |
| 145 | t.Fatal(err) |
| 146 | } |
| 147 | if _, err := store.Put(ctx, hostops.BackfillKey, raw, ""); err != nil { |
| 148 | t.Fatal(err) |
| 149 | } |
| 150 | h := o.Claim.Holder |
| 151 | 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}} |
| 152 | for _, settled := range []bool{false, true} { |
| 153 | v := view |
| 154 | v.Settled = settled |
| 155 | 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 }} |
| 156 | s, err := c.collect(ctx) |
| 157 | if settled { |
| 158 | if !errors.Is(err, hostguard.ErrFenced) { |
| 159 | t.Fatalf("collector admitted a settled installation: %v, items %+v", err, s.Items) |
| 160 | } |
| 161 | continue |
| 162 | } |
| 163 | if err != nil || len(s.Items) != 1 || s.Items[0].Installation != view.Installation { |
| 164 | t.Fatalf("control: the unsettled view: %v, items %+v", err, s.Items) |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |