package host import ( "context" "encoding/json" "errors" "os" "strings" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) func recoverRootRestarts(ctx context.Context, sys unitSystem, optDir string, getenv func(string) string, checks rootRestartChecks, writes hostwrite.Config) error { legacy, err := checks.allowLegacy() if err != nil { return err } if legacy { checks.noteLegacy() // D3 still records root health without a census. Preparing its // public parent must not create or recover bound restart state. return rootstate.EnsurePublicDir(release.RootHealthyDir) } if checks.attempts == nil || checks.prepareRollback == nil { return os.ErrPermission } s, err := checks.attempts() if err != nil { return err } defer s.Close() names, err := s.Names() if err != nil { return err } var failures []error for _, name := range names { if !strings.HasSuffix(name, ".attempt") { continue } body, err := s.Read(name) var a rollbackAttempt if err != nil || json.Unmarshal(body, &a) != nil || name != rollbackJournalKey(a.Host) { failures = append(failures, errors.Join(err, os.ErrPermission)) continue } if a.Settled && a.History == nil { if err := removeRollbackCapabilities(s, a.Host); err != nil { failures = append(failures, err) } continue } if err := resumeRootRestart(ctx, sys, optDir, getenv, checks, writes, s, a); err != nil { failures = append(failures, err) } } return errors.Join(failures...) } func resumeRootRestart(ctx context.Context, sys unitSystem, optDir string, getenv func(string) string, checks rootRestartChecks, writes hostwrite.Config, s rootstate.Store, expected rollbackAttempt) (result error) { unlock, err := s.Lock(ctx, rollbackHostKey(expected.Host)+".lock") if err != nil { return err } defer unlock() current, err := readRollbackAttempt(s, expected.Host) if err != nil || current.ID != expected.ID { return errors.Join(err, os.ErrPermission) } if current.History != nil { return resumeHistoryRestart(ctx, sys, optDir, getenv, checks, writes, s, current) } if current.Settled { return nil } a, b, err := loadRollback(s, expected.Host, checks.now()) if a.ID != expected.ID { return errors.Join(err, os.ErrPermission) } defer func() { result = errors.Join(result, settleRollbackAttempt(s, a)) }() 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) } err = checks.effect(func() error { var err error b, err = checks.prepareRollback(ctx, b) if err != nil { return err } p, err := sys.Show(b.Route.Unit) if err != nil { return err } if p.ActiveState == "inactive" { return errUnitStopped } live, err := checks.live(optDir, a.Host) old := release.VersionPath(optDir, b.Route.Account, b.Version, b.Route.App) if err != nil || (live != a.Target && live != old) { return os.ErrPermission } if !a.Recovering && !a.Failed && p.ActiveState == "active" && p.MainPID > 0 && live == a.Target { checks.verifyOnly = true checks.targetDigest = a.TargetDigest return nil } if a.Recovering && p.ActiveState == "active" && p.MainPID > 0 && live == old { checks.verifyOnly = true } 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 != old { return release.PointLive(optDir, a.Host, b.Route.Account, b.Version, b.Route.App) } return nil }) if err != nil { return err } if checks.verifyOnly && !a.Recovering { b, err = checks.host(ctx, a.Host) if err != nil { return err } return restartBoundUnit(sys, optDir, getenv, b, false, nil, checks) } b, err = checks.rollback(ctx, b) if err != nil { return err } return restartBoundUnit(sys, optDir, getenv, b, true, nil, checks) }