| 1 | package hostrelocate |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "slices" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/boxchoice" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | "github.com/carlosframework/platform/internal/hostops" |
| 13 | ) |
| 14 | |
| 15 | // scopedChooser plugs create's chooser into the fixture's worker, scoped |
| 16 | // to namespace, with every fixture box fresh and eligible on its edges |
| 17 | // file, equally roomy under spec 2's policy (§7), and spa the most loaded. |
| 18 | func scopedChooser(f *fixture, namespace string) RegionChooser { |
| 19 | yes := true |
| 20 | snap := boxchoice.Snapshot{Complete: true} |
| 21 | generation := f.w.Now().Add(-time.Minute).UTC().Format(time.RFC3339) |
| 22 | for _, ref := range []string{"src", "dst", "spa", "sp2"} { |
| 23 | snap.Boxes = append(snap.Boxes, boxchoice.Box{BoxID: ref, EIP: "203.0.113.1", Region: "eu-west-1", Placement: "ie", Fresh: true, |
| 24 | HibernationConfigured: &yes, ServesSharedPool: &yes, AcceptsInbound: &yes, SecretsKeyFP: "fp", Capabilities: []string{boxchoice.Capability}, |
| 25 | Headroom: &boxchoice.Headroom{MemFreeMiB: 4000, MemTotalMiB: 8000, DiskFreeMiB: 50000, DiskTotalMiB: 100000}, |
| 26 | Generation: generation}) |
| 27 | } |
| 28 | loads := map[string]boxchoice.Load{"spa": {Hosts: 9, Apps: map[string]int{"bab/jam": 3}}} |
| 29 | return RegionChooser{Records: f.s, Namespace: namespace, Region: "ie", |
| 30 | Executors: hostops.ExecutorChooser{Directory: f.w.Directory, SecretsKeyFP: "fp", Now: f.w.Now, |
| 31 | Edges: func(context.Context) (boxchoice.Snapshot, error) { return snap, nil }, |
| 32 | Loads: func(context.Context) (map[string]boxchoice.Load, error) { return loads, nil }}} |
| 33 | } |
| 34 | |
| 35 | // Task 2's fix: the chooser chooses only within claim Plan 5's scope, and |
| 36 | // an unscoped one chooses nothing. With the scope set from the worker's |
| 37 | // namespace and the record's pool, the worker picks a target: never the |
| 38 | // source (the least loaded box, so the chooser alone would name it), never |
| 39 | // the dead target, never a box of another namespace. |
| 40 | func TestWorkerChoosesATargetWithinTheHostsScope(t *testing.T) { |
| 41 | f := newFixture(t) |
| 42 | ctx := context.Background() |
| 43 | body := `{"host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}` |
| 44 | if _, err := f.s.Put(ctx, f.claim().Claim.Holder.Key, []byte(body), ""); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | b := f.roster.Boxes["sp2"] |
| 48 | b.Namespace = "other" |
| 49 | f.roster.Boxes["sp2"] = b |
| 50 | f.roster.Revision++ |
| 51 | f.writeRoster() |
| 52 | |
| 53 | unscoped := scopedChooser(f, "") |
| 54 | f.w.Choose, f.w.Eligible = unscoped.Choose, unscoped.Eligible |
| 55 | before := f.claim().ETag |
| 56 | if _, err := f.w.Start(ctx, host, Request{Reason: hostclaim.ReasonDrain, Source: "src", Target: "dst"}); !errors.Is(err, ErrUnsupported) { |
| 57 | t.Fatalf("an unscoped chooser admitted a target: %v", err) |
| 58 | } |
| 59 | |
| 60 | scoped := scopedChooser(f, "platform") |
| 61 | f.w.Choose, f.w.Eligible = scoped.Choose, scoped.Eligible |
| 62 | if _, err := f.w.Start(ctx, host, Request{Reason: hostclaim.ReasonDrain, Source: "src", Target: "sp2"}); !errors.Is(err, hostops.ErrNoEligibleBox) { |
| 63 | t.Fatalf("started on a box of another namespace: %v", err) |
| 64 | } |
| 65 | if f.claim().ETag != before { |
| 66 | t.Fatal("a refused start wrote the claim") |
| 67 | } |
| 68 | if _, err := f.w.Start(ctx, host, Request{Reason: hostclaim.ReasonDrain, Source: "src", Target: "dst"}); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | f.terminate("dst") |
| 72 | o := f.worker(nil) |
| 73 | if o.Claim.Op.Intent["target_box"] != "spa" || o.Claim.Op.Intent["target_enrollment"] != enrollment("spa") || !slices.Contains(o.Claim.Lost, "dst") { |
| 74 | t.Fatalf("replacement: %+v lost %v", o.Claim.Op.Intent, o.Claim.Lost) |
| 75 | } |
| 76 | } |
| 77 | |