| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostactors" |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | // BackfillAuthority deliberately grants only the frozen backfill's adoption. |
| 15 | // Mutation doors use ProductionAuthority once their full construction lands. |
| 16 | type BackfillAuthority struct{ p ProductionAuthority } |
| 17 | |
| 18 | func (a BackfillAuthority) Inventory(ctx context.Context, obs hostclaim.Observed) ([]string, []string, error) { |
| 19 | return a.p.Inventory(ctx, obs) |
| 20 | } |
| 21 | func (a BackfillAuthority) Adoption(ctx context.Context, r Record) (string, string, []string, []string, error) { |
| 22 | return a.p.Adoption(ctx, r) |
| 23 | } |
| 24 | func (a BackfillAuthority) ClaimBackfill() *Backfill { return a.p.Backfill } |
| 25 | |
| 26 | func (a BackfillAuthority) SelectExecutor(context.Context, hostclaim.Observed, Record) (hostclaim.ExecutorSelection, bool, error) { |
| 27 | return hostclaim.ExecutorSelection{}, false, nil // the backfill grants no create (Create refuses) |
| 28 | } |
| 29 | func (a BackfillAuthority) Create(context.Context, hostclaim.Observed, Record, string) error { |
| 30 | return ErrContract |
| 31 | } |
| 32 | func (a BackfillAuthority) Executor(context.Context, hostclaim.Observed, Record) (string, error) { |
| 33 | return "", ErrContract |
| 34 | } |
| 35 | func (a BackfillAuthority) StartDelete(context.Context, hostclaim.Observed, Record) error { |
| 36 | return ErrContract |
| 37 | } |
| 38 | func (a BackfillAuthority) FirstPhase(context.Context, hostclaim.Observed) error { return ErrContract } |
| 39 | func (a BackfillAuthority) PurgeRetirement(context.Context, hostclaim.Observed) error { |
| 40 | return ErrContract |
| 41 | } |
| 42 | func NewBackfillController(s release.Store, d hostactors.Directory, now func() time.Time, newID func() string) Controller { |
| 43 | b := &Backfill{Store: s, Directory: d} |
| 44 | return Controller{Store: s, Now: now, NewID: newID, Authority: BackfillAuthority{ProductionAuthority{Store: s, Directory: d, Backfill: b, Now: now}}} |
| 45 | } |
| 46 | |
| 47 | func ReconcileClaims(ctx context.Context, c Controller, adopt func(context.Context, string) (hostclaim.Observed, error)) error { |
| 48 | if adopt == nil { |
| 49 | return ErrContract |
| 50 | } |
| 51 | var backfill *Backfill |
| 52 | if provider, ok := c.Authority.(interface{ ClaimBackfill() *Backfill }); ok { |
| 53 | backfill = provider.ClaimBackfill() |
| 54 | } |
| 55 | if backfill != nil { |
| 56 | plan, _, err := backfill.readPlan(ctx) |
| 57 | if errors.Is(err, os.ErrNotExist) { |
| 58 | return nil |
| 59 | } |
| 60 | if err != nil { |
| 61 | return err |
| 62 | } |
| 63 | if plan.Phase != "adopting" { |
| 64 | return nil |
| 65 | } |
| 66 | } |
| 67 | apps, err := Apps(ctx, c.Store) |
| 68 | if err != nil { |
| 69 | return err |
| 70 | } |
| 71 | var all []error |
| 72 | // Collect the entire listing before assigning any hostname. A broken page |
| 73 | // cannot authorize an adoption against only the prefix read so far. |
| 74 | var records []Record |
| 75 | seen := map[string]bool{} |
| 76 | for _, app := range apps { |
| 77 | rs, err := Records(ctx, c.Store, app.Account, app.Name) |
| 78 | if err != nil { |
| 79 | return err |
| 80 | } |
| 81 | for _, r := range rs { |
| 82 | if seen[r.Holder.Host] { |
| 83 | return ErrIdentity |
| 84 | } |
| 85 | seen[r.Holder.Host] = true |
| 86 | records = append(records, r) |
| 87 | } |
| 88 | } |
| 89 | for _, listed := range records { |
| 90 | r, err := ReadRecord(ctx, c.Store, listed.Holder.Key) |
| 91 | if errors.Is(err, os.ErrNotExist) { |
| 92 | continue |
| 93 | } |
| 94 | if err != nil { |
| 95 | all = append(all, err) |
| 96 | continue |
| 97 | } |
| 98 | if err = CheckHostQuarantine(ctx, c.Store, r.Holder.Host); err != nil { |
| 99 | all = append(all, err) |
| 100 | continue |
| 101 | } |
| 102 | obs, err := hostclaim.Read(ctx, c.Store, r.Holder.Host) |
| 103 | if err != nil { |
| 104 | all = append(all, err) |
| 105 | continue |
| 106 | } |
| 107 | if !obs.Absent && (obs.Claim.Holder == nil || obs.Claim.Holder.Acquisition != "" || r.Holder.Acquisition == "") { |
| 108 | continue |
| 109 | } |
| 110 | if _, err = adopt(ctx, r.Holder.Key); err != nil { |
| 111 | all = append(all, err) |
| 112 | } |
| 113 | } |
| 114 | return errors.Join(all...) |
| 115 | } |
| 116 | func (c Controller) ReconcileClaims(ctx context.Context) error { |
| 117 | return ReconcileClaims(ctx, c, func(ctx context.Context, key string) (hostclaim.Observed, error) { |
| 118 | rec, err := ReadRecord(ctx, c.Store, key) |
| 119 | if err != nil { |
| 120 | return hostclaim.Observed{}, err |
| 121 | } |
| 122 | return c.Adopt(ctx, rec) |
| 123 | }) |
| 124 | } |
| 125 | |
| 126 | func (a ProductionAuthority) ClaimBackfill() *Backfill { return a.Backfill } |
| 127 | |