| 1 | package hostrelocate |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "slices" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/hostops" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | // RegionChooser gives the worker its Choose and Eligible from create's |
| 15 | // chooser (hostops.ExecutorChooser), so a relocation's target comes from |
| 16 | // the same complete eligible set a create's executor does (spec 1 §2, |
| 17 | // §5.2). Like ProductionAuthority.SelectExecutor it chooses within claim |
| 18 | // Plan 5's scope, set on every call: Namespace, and the pool of the host's |
| 19 | // authoritative record. The placement is Region, the worker's: a |
| 20 | // relocation never leaves it, and Worker.Step acts only on its own |
| 21 | // region's operations. |
| 22 | type RegionChooser struct { |
| 23 | Executors hostops.ExecutorChooser |
| 24 | // Records is the deployment (control) bucket holding the host's |
| 25 | // authoritative record at holder.key. |
| 26 | Records release.Store |
| 27 | // Namespace is the roster namespace of the box the worker runs on. An |
| 28 | // empty one refuses: the chooser would choose nothing. |
| 29 | Namespace string |
| 30 | Region string |
| 31 | } |
| 32 | |
| 33 | // Choose names a target for obs's host, never one in exclude. |
| 34 | // ExecutorChooser already leaves out the claim's lost boxes and every box |
| 35 | // owing a park-local chore for the host; exclude adds the worker's own |
| 36 | // (the source, the current target, the targets this operation replaced). |
| 37 | // They are passed as the lost list of a copy of the observation: that list |
| 38 | // is the only exclusion the chooser reads, and the copy is never written. |
| 39 | func (r RegionChooser) Choose(ctx context.Context, obs hostclaim.Observed, exclude []string) (hostclaim.Target, error) { |
| 40 | x, rec, err := r.scoped(ctx, obs) |
| 41 | if err != nil { |
| 42 | return hostclaim.Target{}, err |
| 43 | } |
| 44 | c := *obs.Claim |
| 45 | c.Lost = append(slices.Clone(c.Lost), exclude...) |
| 46 | obs.Claim = &c |
| 47 | sel, err := x.Choose(ctx, obs, rec) |
| 48 | if err != nil { |
| 49 | return hostclaim.Target{}, err |
| 50 | } |
| 51 | return hostclaim.Target{Box: sel.Box, Enrollment: sel.Enrollment}, nil |
| 52 | } |
| 53 | |
| 54 | // Eligible refuses box unless it is in the complete eligible set for obs's |
| 55 | // host now (ExecutorChooser.Permits within the same scope). |
| 56 | func (r RegionChooser) Eligible(ctx context.Context, obs hostclaim.Observed, box string) error { |
| 57 | x, rec, err := r.scoped(ctx, obs) |
| 58 | if err != nil { |
| 59 | return err |
| 60 | } |
| 61 | return x.Permits(ctx, obs, rec, box) |
| 62 | } |
| 63 | |
| 64 | // scoped reads the host's authoritative record and returns the chooser |
| 65 | // scoped to Namespace and the record's pool, with the record placed in |
| 66 | // Region. |
| 67 | func (r RegionChooser) scoped(ctx context.Context, obs hostclaim.Observed) (hostops.ExecutorChooser, hostops.Record, error) { |
| 68 | if r.Records == nil || r.Namespace == "" || r.Region == "" || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { |
| 69 | return hostops.ExecutorChooser{}, hostops.Record{}, ErrUnsupported |
| 70 | } |
| 71 | rec, err := hostops.ReadRecord(ctx, r.Records, obs.Claim.Holder.Key) |
| 72 | if err != nil { |
| 73 | return hostops.ExecutorChooser{}, hostops.Record{}, err |
| 74 | } |
| 75 | if rec.Holder.Host != obs.Host || rec.Holder.Family != obs.Claim.Holder.Family { |
| 76 | return hostops.ExecutorChooser{}, hostops.Record{}, fmt.Errorf("%w: record %s is not %s's", ErrRefused, obs.Claim.Holder.Key, obs.Host) |
| 77 | } |
| 78 | var shape struct { |
| 79 | Pool string `json:"pool"` |
| 80 | } |
| 81 | if err := json.Unmarshal(rec.Body, &shape); err != nil { |
| 82 | return hostops.ExecutorChooser{}, hostops.Record{}, err |
| 83 | } |
| 84 | rec.Holder.Region = r.Region |
| 85 | x := r.Executors |
| 86 | x.Namespace, x.Pool = r.Namespace, shape.Pool |
| 87 | return x, rec, nil |
| 88 | } |
| 89 | |