package host import ( "context" "errors" "log" "os" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type historyRollbackLink struct { TargetDigest string Binding release.HistoryBinding Attempt release.HistoryAttempt Independent bool } func linkRootHistory(h *release.RootHistoryPass, checks rootRestartChecks, optDir string, getenv func(string) string) { h.Prepare = func(ctx context.Context, binding release.HistoryBinding, attempt release.HistoryAttempt, identity hostwrite.Identity) error { b, err := checks.host(ctx, binding.Host) if err != nil { return err } if b.Source != attempt.Source || b.Route.Account != binding.Account || b.Route.App != binding.App { return os.ErrPermission } pending, err := h.History.Pending(binding) if err != nil || pending == nil || *pending != attempt { return errors.Join(err, os.ErrPermission) } s, err := checks.attempts() if err != nil { return err } defer s.Close() unlock, err := s.Lock(ctx, rollbackHostKey(binding.Host)+".lock") if err != nil { return err } defer unlock() live, err := checks.live(optDir, binding.Host) if err != nil || live != release.VersionPath(optDir, binding.Account, b.Version, binding.App) { return errors.Join(err, os.ErrPermission) } _, err = captureRollback(s, b, checks.now(), unitSettleTimeout(getenv), release.VersionPath(optDir, binding.Account, attempt.Version, binding.App), identity, historyRollbackLink{Binding: binding, Attempt: attempt}) return err } } func historyRestartAttempt(s rootstate.Store, b proofBinding, c rootRestartChecks, target string, getenv func(string) string) (rollbackAttempt, error) { a, err := readRollbackAttempt(s, b.Route.Host) if err != nil && !errors.Is(err, os.ErrNotExist) { return a, err } if err == nil && a.History != nil && !a.Settled { loaded, old, err := loadRollback(s, b.Route.Host, c.now()) if err != nil { return a, err } if loaded.ID != a.ID || a.Identity != c.identity || a.Target != target || old.Source != b.Source || old.Version != b.Version || old.Digest != b.Digest || old.Invocation != b.Invocation || old.PID != b.PID || old.Start != b.Start || a.Failed || a.Recovering { return a, os.ErrPermission } return a, nil } return captureRollback(s, b, c.now(), unitSettleTimeout(getenv), target, c.identity, historyRollbackLink{TargetDigest: c.targetDigest}) } func (c rootRestartChecks) settleHistory(ctx context.Context, a rollbackAttempt, rollback bool) error { if a.History == nil { return nil } if a.Independent { if c.history == nil { return nil } h, err := c.history() if err == nil { defer h.History.Store.Close() var outcome release.HistoryRecovery outcome, err = recoveryHistoryOutcome(a) if err == nil { err = h.History.RetainRecovery(outcome) if err == nil && h.Reconstruct != nil { err = h.Reconstruct(ctx, outcome.Binding) } } } if err != nil { log.Print(err) } return nil } if c.history == nil { return os.ErrPermission } h, err := c.history() if err != nil { return err } defer h.History.Store.Close() pending, err := h.History.Pending(a.HistoryBinding) if err != nil { return err } if pending == nil { return nil } if *pending != *a.History { return os.ErrPermission } b, err := c.host(ctx, a.Host) if err != nil { return err } if b.Source != a.History.Source || b.Route.Account != a.HistoryBinding.Account || b.Route.App != a.HistoryBinding.App { return os.ErrPermission } if !rollback { if b.Version != a.History.Version || b.Digest != a.History.SHA256 { return os.ErrPermission } return h.CompleteAttempt(ctx, b.Route, *a.History) } if b.Version != a.Previous || b.Digest != a.PreviousDigest { return os.ErrPermission } if _, err := h.Installed(ctx, b.Route, b.Version); err != nil { return err } again, err := c.host(ctx, a.Host) if err != nil || again.Source != b.Source || again.Version != b.Version || again.Digest != b.Digest || again.PID != b.PID || again.Start != b.Start || again.Invocation != b.Invocation { return errors.Join(err, os.ErrPermission) } ref := release.Refusal{Host: a.Host, App: b.Route.App, Version: a.History.Version, SHA256: a.History.SHA256, Platform: a.History.Platform, PointerPromotedAt: a.History.PromotedAt, RefusedAt: c.now().UTC(), Kind: release.RefusalNotUp, Recovered: true, RestoredVersion: b.Version} return h.History.Settle(a.HistoryBinding, a.History.ID, &ref) } func resumeHistoryRestart(ctx context.Context, sys unitSystem, optDir string, getenv func(string) string, checks rootRestartChecks, writes hostwrite.Config, s rootstate.Store, a rollbackAttempt) error { checks.verifyOnly = false if a.Settled { if (a.Failed || a.Recovering) && !a.Verified { return nil } return checks.settleHistory(ctx, a, a.Recovering) } legacy, err := checks.allowLegacy() if err != nil { return err } if legacy { if a.Independent { return checks.settleHistory(ctx, a, a.Recovering) } return nil } _, old, err := loadRollback(s, a.Host, checks.now()) if err != nil { return err } w, err := writes.Select(ctx, a.Host, "release") if err != nil { return err } checks.identity = a.Identity checks.effect = func(fn func() error) error { return w.With(ctx, a.Identity, fn) } var forward bool err = checks.effect(func() error { var err error old, err = checks.prepareRollback(ctx, old) if err != nil { return err } p, err := sys.Show(old.Route.Unit) if err != nil { return err } if p.ActiveState == "inactive" { return errUnitStopped } live, err := checks.live(optDir, a.Host) previous := release.VersionPath(optDir, old.Route.Account, old.Version, old.Route.App) if err != nil || (live != previous && live != a.Target) { return os.ErrPermission } if p.ActiveState == "active" && p.MainPID > 0 { observed, err := checks.host(ctx, a.Host) if err != nil && checks.recovered != nil { observed, err = checks.recovered(ctx, old) } if err != nil { return err } if observed.Source != old.Source { return os.ErrPermission } if live == a.Target && observed.Version == a.History.Version && observed.Digest == a.History.SHA256 && !a.Failed && !a.Recovering { forward, checks.verifyOnly = true, true return nil } if observed.Version == old.Version && observed.Digest == old.Digest { checks.verifyOnly = true } else if !a.Failed || observed.Version != a.History.Version || observed.Digest != a.History.SHA256 { return os.ErrPermission } } else if p.ActiveState != "failed" && p.MainPID != 0 && !a.Failed { return os.ErrPermission } a.Recovering = true if err := writeRollbackState(s, rollbackJournalKey(a.Host), a); err != nil { return err } if live != previous { return release.PointLive(optDir, a.Host, old.Route.Account, old.Version, old.Route.App) } return nil }) if err != nil { return err } checks.targetDigest = a.PreviousDigest if forward { checks.targetDigest = a.History.SHA256 } if err := restartBoundUnit(sys, optDir, getenv, old, !forward, nil, checks); err != nil { if forward { a.Failed = true if writeErr := writeRollbackState(s, rollbackJournalKey(a.Host), a); writeErr != nil { return errors.Join(err, writeErr) } return resumeHistoryRestart(ctx, sys, optDir, getenv, checks, writes, s, a) } settledErr := settleRollbackAttempt(s, a) if settledErr == nil && a.Independent { a.Settled = true _ = checks.settleHistory(ctx, a, !forward) } return errors.Join(err, settledErr) } a.Verified = true if err := settleRollbackAttempt(s, a); err != nil { return err } a.Settled = true return checks.settleHistory(ctx, a, !forward) }