package host import ( "context" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "strings" "time" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) const rollbackMargin = 30 * time.Second const rootRestartState = "/var/lib/carlos-release/private/restarts" type rollbackAttempt struct { ID string Host string Created time.Time Deadline time.Time Failed bool Recovering bool Settled bool Verified bool `json:",omitempty"` Independent bool `json:",omitempty"` Target string Previous string `json:",omitempty"` PreviousDigest string `json:",omitempty"` Identity hostwrite.Identity History *release.HistoryAttempt `json:",omitempty"` HistoryBinding release.HistoryBinding `json:",omitempty"` TargetDigest string } type rollbackCapability struct { Attempt rollbackAttempt Binding proofBinding } func rollbackHostKey(host string) string { h := sha256.Sum256([]byte(host)) return hex.EncodeToString(h[:]) } func rollbackJournalKey(host string) string { return rollbackHostKey(host) + ".attempt" } func rollbackCapabilityKey(host, id string) string { return rollbackHostKey(host) + "." + id + ".cap" } func writeRollbackState(s rootstate.Store, key string, v any) error { b, err := json.Marshal(v) if err != nil { return err } return s.Write(key, b) } func readRollbackAttempt(s rootstate.Store, host string) (rollbackAttempt, error) { var a rollbackAttempt b, err := s.Read(rollbackJournalKey(host)) if err != nil { return a, err } if json.Unmarshal(b, &a) != nil || a.Host != host || len(a.ID) != 32 { return a, os.ErrPermission } if _, err := hex.DecodeString(a.ID); err != nil { return a, os.ErrPermission } return a, nil } func settleRollbackAttempt(s rootstate.Store, a rollbackAttempt) error { if !a.Settled { a.Settled = true if a.Independent { if err := writeRollbackState(s, recoveryResultKey(a), a); err != nil { return err } } if err := writeRollbackState(s, rollbackJournalKey(a.Host), a); err != nil { return err } } return removeRollbackCapabilities(s, a.Host) } func captureRollback(s rootstate.Store, b proofBinding, now time.Time, timeout time.Duration, target string, identity hostwrite.Identity, link ...historyRollbackLink) (rollbackAttempt, error) { if b.Source == "" || b.Version == "" || b.Digest == "" { return rollbackAttempt{}, os.ErrPermission } if prior, err := readRollbackAttempt(s, b.Route.Host); err == nil { if !prior.Settled && !now.After(prior.Deadline.Add(rollbackMargin)) { return rollbackAttempt{}, os.ErrPermission } if err := settleRollbackAttempt(s, prior); err != nil { return rollbackAttempt{}, err } } else if !errors.Is(err, os.ErrNotExist) { return rollbackAttempt{}, err } id := make([]byte, 16) if _, err := rand.Read(id); err != nil { return rollbackAttempt{}, err } a := rollbackAttempt{ID: hex.EncodeToString(id), Host: b.Route.Host, Created: now, Deadline: now.Add(2 * timeout), Target: target, Identity: identity, Previous: b.Version, PreviousDigest: b.Digest} if len(link) > 1 { return rollbackAttempt{}, os.ErrPermission } if len(link) == 1 { l := link[0] a.TargetDigest = l.TargetDigest if l.Attempt.ID != "" { if l.Binding.Host != b.Route.Host || l.Binding.Account != b.Route.Account || l.Binding.App != b.Route.App || l.Attempt.Source != b.Source { return rollbackAttempt{}, os.ErrPermission } if l.Independent { l.Attempt.ID = a.ID } a.History, a.HistoryBinding = &l.Attempt, l.Binding a.Independent = l.Independent a.TargetDigest = l.Attempt.SHA256 } } if err := writeRollbackState(s, rollbackCapabilityKey(a.Host, a.ID), rollbackCapability{Attempt: a, Binding: b}); err != nil { return rollbackAttempt{}, err } if err := writeRollbackState(s, rollbackJournalKey(a.Host), a); err != nil { return rollbackAttempt{}, errors.Join(err, s.Remove(rollbackCapabilityKey(a.Host, a.ID))) } return a, nil } func loadRollback(s rootstate.Store, host string, now time.Time) (rollbackAttempt, proofBinding, error) { a, err := readRollbackAttempt(s, host) if err != nil { return rollbackAttempt{}, proofBinding{}, errors.Join(err, removeRollbackCapabilities(s, host)) } if a.Settled || now.Before(a.Created) || now.After(a.Deadline.Add(rollbackMargin)) { return a, proofBinding{}, errors.Join(os.ErrPermission, settleRollbackAttempt(s, a)) } b, err := s.Read(rollbackCapabilityKey(host, a.ID)) if err != nil { return a, proofBinding{}, err } var cap rollbackCapability if json.Unmarshal(b, &cap) != nil || cap.Attempt.ID != a.ID || cap.Attempt.TargetDigest != a.TargetDigest || cap.Attempt.Independent != a.Independent || cap.Attempt.HistoryBinding != a.HistoryBinding || !sameHistoryAttempt(cap.Attempt.History, a.History) || cap.Attempt.Previous != a.Previous || cap.Attempt.PreviousDigest != a.PreviousDigest || cap.Attempt.Host != host || cap.Attempt.Target != a.Target || cap.Attempt.Identity != a.Identity || !cap.Attempt.Deadline.Equal(a.Deadline) || !cap.Attempt.Created.Equal(a.Created) || cap.Binding.Route.Host != host { return a, proofBinding{}, os.ErrPermission } return a, cap.Binding, nil } func (c rootRestartChecks) finish(host string) error { if c.attempts == nil || c.started == nil { return os.ErrPermission } expected, ok := c.started.Load(host) if !ok { return nil } defer c.started.Delete(host) s, err := c.attempts() if err != nil { return err } defer s.Close() unlock, err := s.Lock(context.Background(), rollbackHostKey(host)+".lock") if err != nil { return err } defer unlock() a, err := readRollbackAttempt(s, host) if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } if a.ID != expected { return os.ErrPermission } return settleRollbackAttempt(s, a) } func removeRollbackCapabilities(s rootstate.Store, host string) error { names, err := s.Names() if err != nil { return err } for _, name := range names { if !strings.HasPrefix(name, rollbackHostKey(host)+".") || !strings.HasSuffix(name, ".cap") { continue } if err := s.Remove(name); err != nil { return err } } return nil } func sameHistoryAttempt(a, b *release.HistoryAttempt) bool { if a == nil || b == nil { return a == nil && b == nil } return *a == *b }