package release import ( "context" "crypto/rand" "encoding/hex" "os" "slices" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" ) type RootHistoryPass struct { Creation func(context.Context, registry.Route) (string, error) History *RootHistory Resolve func(context.Context, registry.Route) (HistoryBinding, string, error) Installed func(context.Context, registry.Route, string) (string, error) Prepare func(context.Context, HistoryBinding, HistoryAttempt, hostwrite.Identity) error Reconstruct func(context.Context, HistoryBinding) error } type historySelection struct { binding HistoryBinding source string creation string } func WithRecovery(fn func(context.Context, registry.Route) (bool, error), legacy ...func() (bool, error)) PassOption { return func(c *passConfig) { c.recovery = fn if len(legacy) == 1 { c.legacyRecovery = legacy[0] } } } func WithRootHistory(h *RootHistoryPass) PassOption { return func(c *passConfig) { c.rootHistory = h c.requireRootHistory = true } } func (h *RootHistoryPass) resolve(ctx context.Context, rt registry.Route) (historySelection, error) { if h == nil || h.History == nil || h.Resolve == nil { return historySelection{}, os.ErrPermission } b, source, err := h.Resolve(ctx, rt) if err != nil { return historySelection{}, err } if !b.valid() || source == "" || b.Host != rt.Host || b.Account != RouteAccount(rt) || b.App != rt.App { return historySelection{}, os.ErrPermission } creation := "" if h.Creation != nil { creation, err = h.Creation(ctx, rt) if err != nil { return historySelection{}, err } } return historySelection{binding: b, source: source, creation: creation}, nil } func (h *RootHistoryPass) selectRoute(ctx context.Context, rt registry.Route) (historySelection, error) { sel, err := h.resolve(ctx, rt) if err != nil { return sel, err } if h.Installed == nil { return sel, os.ErrPermission } if h.Reconstruct != nil { if err := h.Reconstruct(ctx, sel.binding); err != nil { return sel, err } if err := h.recheck(ctx, rt, sel); err != nil { return sel, err } } if _, err := h.History.Programs(sel.binding); err != nil { return sel, err } _, err = h.History.Refusals(sel.binding) return sel, err } func (c passConfig) recheckHistory(ctx context.Context, rt registry.Route) error { if !c.requireRootHistory { return nil } return c.rootHistory.recheck(ctx, rt, c.historySelected[rt.Host]) } func (c passConfig) bindProof(ctx context.Context, optDir string, rt registry.Route, ptr *ChannelPointer, v proofVerdict, cur string, now time.Time) bool { if !c.requireRootHistory { return c.proof.bind(optDir, rt, ptr, v, cur, now) } return c.proof.bindRecording(optDir, rt, ptr, v, cur, now, func(r Refusal) error { err := c.recordRefusal(ctx, rt, r) c.historyError(err) return err }) } func (h *RootHistoryPass) recheck(ctx context.Context, rt registry.Route, want historySelection) error { got, err := h.resolve(ctx, rt) if err != nil { return err } if got != want { return os.ErrPermission } return nil } func (h *RootHistoryPass) begin(ctx context.Context, rt registry.Route, sel historySelection, ptr *ChannelPointer, digest string, now time.Time, identity hostwrite.Identity) (string, error) { if err := h.recheck(ctx, rt, sel); err != nil { return "", err } var id [16]byte if _, err := rand.Read(id[:]); err != nil { return "", err } a := HistoryAttempt{ID: hex.EncodeToString(id[:]), Version: ptr.Version, Source: sel.source, Started: now.UTC(), SHA256: digest, Platform: adoptPlatform, PromotedAt: ptr.PromotedAt} if err := h.History.Begin(sel.binding, a); err != nil { return "", err } if h.Prepare != nil { if err := h.Prepare(ctx, sel.binding, a, identity); err != nil { return "", err } } return a.ID, nil } func (h *RootHistoryPass) CompleteAttempt(ctx context.Context, rt registry.Route, a HistoryAttempt) error { sel, err := h.resolve(ctx, rt) if err != nil { return err } if sel.source != a.Source { return os.ErrPermission } return h.complete(ctx, rt, sel, a.ID, a.Version) } func (h *RootHistoryPass) complete(ctx context.Context, rt registry.Route, sel historySelection, id, version string) error { if h.Installed == nil { return os.ErrPermission } if err := h.recheck(ctx, rt, sel); err != nil { return err } program, err := h.Installed(ctx, rt, version) if err != nil { return err } if err := h.recheck(ctx, rt, sel); err != nil { return err } r, err := h.History.read(sel.binding) if err != nil { return err } if r.Attempt == nil || r.Attempt.ID != id || r.Attempt.Version != version || r.Attempt.Source != sel.source || r.Revision == ^uint64(0) { return os.ErrPermission } if program != "" && !slices.Contains(r.Programs.Values, program) { r.Programs.Values = append(r.Programs.Values, program) } r.Revision++ r.Programs.Revision = r.Revision r.Refusals.Values = slices.DeleteFunc(r.Refusals.Values, func(ref Refusal) bool { return ref.Version == version }) r.Refusals.Revision = r.Revision r.Attempt = nil return h.History.write(r) } func (c passConfig) historyError(err error) { if err != nil && c.restartErrors != nil { *c.restartErrors = append(*c.restartErrors, err) } } func (c passConfig) recordRefusal(ctx context.Context, rt registry.Route, r Refusal) error { if c.requireRootHistory { sel, ok := c.historySelected[rt.Host] if !ok { return os.ErrPermission } if err := c.rootHistory.recheck(ctx, rt, sel); err != nil { return err } if err := c.rootHistory.History.AddRefusal(sel.binding, r); err != nil { return err } } if c.bakeDir != "" { return WriteRefusal(c.bakeDir, r) } return nil }