package host import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "os" "path/filepath" "reflect" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func (n namedUnitRecovery) recover(ctx context.Context, host string, c rootRestartChecks, getenv func(string) string, reg *registry.Registry) (bool, error) { if rt, err := reg.Get(host); err == nil && rt != nil && (rt.Unit == "" || rt.Unit == activator.UnitName(host)) { return false, nil } c.preserveOutcome = true n.history, n.attempts = c.history, c.attempts legacy, err := c.allowLegacy() if err != nil { return true, err } if legacy { return false, nil } if n.source == nil || n.readLive == nil { return true, os.ErrPermission } record, _, err := n.source.findRecord(ctx, host) if err != nil { return true, err } if record.Unit == "" { return false, nil } if handled, err := n.repair(ctx, host); handled || err != nil { return handled, err } rec, b, props, err := n.source.unitBinding(ctx, host) if err != nil { return true, err } if props.ActiveState != "active" || props.MainPID <= 0 { return true, os.ErrPermission } old, err := n.source.forHost(ctx, host) if err != nil || old.Source != b.Source || !old.authorizedRecovery() { return true, errors.Join(err, os.ErrPermission) } live := release.LiveLink(n.source.optDir, host) before, err := n.readLive(live) if err != nil || before != release.VersionPath(n.source.optDir, rec.Account, old.Version, rec.App) { return true, errors.Join(err, os.ErrPermission) } pointer, manifest, err := release.StageSignedHead(ctx, n.source.store, n.pub, n.ratchet, n.source.optDir, b.Route) if err != nil { return true, err } if rec.Pin != "" && rec.Pin != pointer.Version { return true, os.ErrPermission } target := release.VersionPath(n.source.optDir, rec.Account, pointer.Version, rec.App) digest := manifest.Artifacts[release.AdoptPlatform()].SHA256 work, err := n.writes.Select(ctx, host, "release") if err != nil { return true, err } id := hostwrite.Identity{} if n.writes.Enabled { raw, err := json.Marshal(manifest) if err != nil { return true, err } sum := sha256.Sum256(raw) id.Release = "sha256:" + hex.EncodeToString(sum[:]) } effect := func(fn func() error) error { return work.With(ctx, id, fn) } validate := func() error { current, err := n.source.forHost(ctx, host) if err != nil { return err } if current.Source != old.Source || current.Version != old.Version || current.Digest != old.Digest || current.PID != old.PID || current.Start != old.Start || current.Invocation != old.Invocation { return os.ErrPermission } fresh, err := release.Channel(ctx, n.source.store, n.pub, rec.Account, rec.App, b.Route.Channel) if err != nil { return err } if !reflect.DeepEqual(fresh, pointer) { return os.ErrPermission } if err := n.checkHistory(b, pointer); err != nil { return err } still, err := n.readLive(live) if err != nil || still != before { return os.ErrPermission } f, err := n.source.openFile(target) if err != nil { return err } hash := sha256.New() _, readErr := io.Copy(hash, f) f.Close() if readErr != nil { return readErr } if hex.EncodeToString(hash.Sum(nil)) != digest { return os.ErrPermission } return nil } if before == target { c.effect, c.verifyOnly = effect, true if err := effect(validate); err != nil { return true, err } if err := restartBoundUnit(n.source.sys, n.source.optDir, getenv, old, false, nil, c); err != nil { return true, err } return true, effect(func() error { if err := validate(); err != nil { return err } current, err := reg.Get(host) if err != nil || current == nil { return errors.Join(err, os.ErrPermission) } if current.Version == pointer.Version { return nil } return reg.SetVersions(host, pointer.Version, current.Version) }) } if c.attempts == nil || c.now == nil { return true, os.ErrPermission } s, err := c.attempts() if err != nil { return true, err } defer s.Close() unlock, err := s.Lock(ctx, rollbackHostKey(host)+".lock") if err != nil { return true, err } defer unlock() var superseded *rollbackAttempt if prior, err := readRollbackAttempt(s, host); err == nil { sameTarget := prior.History != nil && prior.History.Source == old.Source && prior.History.Version == pointer.Version && prior.History.SHA256 == digest && prior.History.PromotedAt.Equal(pointer.PromotedAt) if sameTarget && (!prior.Settled || prior.Failed) { return true, os.ErrPermission } if !prior.Settled { superseded = &prior } } else if !errors.Is(err, os.ErrNotExist) { return true, err } var attempt rollbackAttempt err = effect(func() error { if err := validate(); err != nil { return err } if superseded != nil { if err := settleRollbackAttempt(s, *superseded); err != nil { return err } if superseded.Independent { superseded.Settled = true _ = c.settleHistory(ctx, *superseded, superseded.Recovering) } } var err error attempt, err = captureRollback(s, old, c.now(), unitSettleTimeout(getenv), target, id, historyRollbackLink{ Binding: release.HistoryBinding{Host: host, Account: rec.Account, App: rec.App}, Attempt: release.HistoryAttempt{ID: "signed-recovery", Source: old.Source, Version: pointer.Version, SHA256: digest, Platform: release.AdoptPlatform(), Started: c.now().UTC(), PromotedAt: pointer.PromotedAt}, Independent: true, }) if err != nil { return err } if err := release.PointLive(n.source.optDir, host, rec.Account, pointer.Version, rec.App); err != nil { return err } dir, err := os.Open(filepath.Dir(live)) if err != nil { return err } defer dir.Close() return dir.Sync() }) if err != nil { return true, err } c.identity, c.effect, c.targetDigest = id, effect, digest if err := restartBoundUnit(n.source.sys, n.source.optDir, getenv, old, false, nil, c); err != nil { attempt.Failed = true if writeErr := writeRollbackState(s, rollbackJournalKey(host), attempt); writeErr != nil { return true, errors.Join(err, writeErr) } return true, errors.Join(err, resumeHistoryRestart(ctx, n.source.sys, n.source.optDir, getenv, c, n.writes, s, attempt)) } attempt.Verified = true if err := settleRollbackAttempt(s, attempt); err != nil { return true, err } attempt.Settled = true if err := c.settleHistory(ctx, attempt, false); err != nil { return true, err } return true, effect(func() error { current, err := c.host(ctx, host) if err != nil || current.Source != old.Source || current.Version != pointer.Version || current.Digest != digest { return errors.Join(err, os.ErrPermission) } return reg.SetVersions(host, pointer.Version, old.Version) }) }