package hostrelocate import ( "context" "encoding/json" "fmt" "slices" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/release" ) // RegionChooser gives the worker its Choose and Eligible from create's // chooser (hostops.ExecutorChooser), so a relocation's target comes from // the same complete eligible set a create's executor does (spec 1 §2, // §5.2). Like ProductionAuthority.SelectExecutor it chooses within claim // Plan 5's scope, set on every call: Namespace, and the pool of the host's // authoritative record. The placement is Region, the worker's: a // relocation never leaves it, and Worker.Step acts only on its own // region's operations. type RegionChooser struct { Executors hostops.ExecutorChooser // Records is the deployment (control) bucket holding the host's // authoritative record at holder.key. Records release.Store // Namespace is the roster namespace of the box the worker runs on. An // empty one refuses: the chooser would choose nothing. Namespace string Region string } // Choose names a target for obs's host, never one in exclude. // ExecutorChooser already leaves out the claim's lost boxes and every box // owing a park-local chore for the host; exclude adds the worker's own // (the source, the current target, the targets this operation replaced). // They are passed as the lost list of a copy of the observation: that list // is the only exclusion the chooser reads, and the copy is never written. func (r RegionChooser) Choose(ctx context.Context, obs hostclaim.Observed, exclude []string) (hostclaim.Target, error) { x, rec, err := r.scoped(ctx, obs) if err != nil { return hostclaim.Target{}, err } c := *obs.Claim c.Lost = append(slices.Clone(c.Lost), exclude...) obs.Claim = &c sel, err := x.Choose(ctx, obs, rec) if err != nil { return hostclaim.Target{}, err } return hostclaim.Target{Box: sel.Box, Enrollment: sel.Enrollment}, nil } // Eligible refuses box unless it is in the complete eligible set for obs's // host now (ExecutorChooser.Permits within the same scope). func (r RegionChooser) Eligible(ctx context.Context, obs hostclaim.Observed, box string) error { x, rec, err := r.scoped(ctx, obs) if err != nil { return err } return x.Permits(ctx, obs, rec, box) } // scoped reads the host's authoritative record and returns the chooser // scoped to Namespace and the record's pool, with the record placed in // Region. func (r RegionChooser) scoped(ctx context.Context, obs hostclaim.Observed) (hostops.ExecutorChooser, hostops.Record, error) { if r.Records == nil || r.Namespace == "" || r.Region == "" || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { return hostops.ExecutorChooser{}, hostops.Record{}, ErrUnsupported } rec, err := hostops.ReadRecord(ctx, r.Records, obs.Claim.Holder.Key) if err != nil { return hostops.ExecutorChooser{}, hostops.Record{}, err } if rec.Holder.Host != obs.Host || rec.Holder.Family != obs.Claim.Holder.Family { return hostops.ExecutorChooser{}, hostops.Record{}, fmt.Errorf("%w: record %s is not %s's", ErrRefused, obs.Claim.Holder.Key, obs.Host) } var shape struct { Pool string `json:"pool"` } if err := json.Unmarshal(rec.Body, &shape); err != nil { return hostops.ExecutorChooser{}, hostops.Record{}, err } rec.Holder.Region = r.Region x := r.Executors x.Namespace, x.Pool = r.Namespace, shape.Pool return x, rec, nil }