package hostops import ( "context" "errors" "os" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/release" ) // BackfillAuthority deliberately grants only the frozen backfill's adoption. // Mutation doors use ProductionAuthority once their full construction lands. type BackfillAuthority struct{ p ProductionAuthority } func (a BackfillAuthority) Inventory(ctx context.Context, obs hostclaim.Observed) ([]string, []string, error) { return a.p.Inventory(ctx, obs) } func (a BackfillAuthority) Adoption(ctx context.Context, r Record) (string, string, []string, []string, error) { return a.p.Adoption(ctx, r) } func (a BackfillAuthority) ClaimBackfill() *Backfill { return a.p.Backfill } func (a BackfillAuthority) SelectExecutor(context.Context, hostclaim.Observed, Record) (hostclaim.ExecutorSelection, bool, error) { return hostclaim.ExecutorSelection{}, false, nil // the backfill grants no create (Create refuses) } func (a BackfillAuthority) Create(context.Context, hostclaim.Observed, Record, string) error { return ErrContract } func (a BackfillAuthority) Executor(context.Context, hostclaim.Observed, Record) (string, error) { return "", ErrContract } func (a BackfillAuthority) StartDelete(context.Context, hostclaim.Observed, Record) error { return ErrContract } func (a BackfillAuthority) FirstPhase(context.Context, hostclaim.Observed) error { return ErrContract } func (a BackfillAuthority) PurgeRetirement(context.Context, hostclaim.Observed) error { return ErrContract } func NewBackfillController(s release.Store, d hostactors.Directory, now func() time.Time, newID func() string) Controller { b := &Backfill{Store: s, Directory: d} return Controller{Store: s, Now: now, NewID: newID, Authority: BackfillAuthority{ProductionAuthority{Store: s, Directory: d, Backfill: b, Now: now}}} } func ReconcileClaims(ctx context.Context, c Controller, adopt func(context.Context, string) (hostclaim.Observed, error)) error { if adopt == nil { return ErrContract } var backfill *Backfill if provider, ok := c.Authority.(interface{ ClaimBackfill() *Backfill }); ok { backfill = provider.ClaimBackfill() } if backfill != nil { plan, _, err := backfill.readPlan(ctx) if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } if plan.Phase != "adopting" { return nil } } apps, err := Apps(ctx, c.Store) if err != nil { return err } var all []error // Collect the entire listing before assigning any hostname. A broken page // cannot authorize an adoption against only the prefix read so far. var records []Record seen := map[string]bool{} for _, app := range apps { rs, err := Records(ctx, c.Store, app.Account, app.Name) if err != nil { return err } for _, r := range rs { if seen[r.Holder.Host] { return ErrIdentity } seen[r.Holder.Host] = true records = append(records, r) } } for _, listed := range records { r, err := ReadRecord(ctx, c.Store, listed.Holder.Key) if errors.Is(err, os.ErrNotExist) { continue } if err != nil { all = append(all, err) continue } if err = CheckHostQuarantine(ctx, c.Store, r.Holder.Host); err != nil { all = append(all, err) continue } obs, err := hostclaim.Read(ctx, c.Store, r.Holder.Host) if err != nil { all = append(all, err) continue } if !obs.Absent && (obs.Claim.Holder == nil || obs.Claim.Holder.Acquisition != "" || r.Holder.Acquisition == "") { continue } if _, err = adopt(ctx, r.Holder.Key); err != nil { all = append(all, err) } } return errors.Join(all...) } func (c Controller) ReconcileClaims(ctx context.Context) error { return ReconcileClaims(ctx, c, func(ctx context.Context, key string) (hostclaim.Observed, error) { rec, err := ReadRecord(ctx, c.Store, key) if err != nil { return hostclaim.Observed{}, err } return c.Adopt(ctx, rec) }) } func (a ProductionAuthority) ClaimBackfill() *Backfill { return a.Backfill }