package host import ( "context" "errors" "log" "os" "path/filepath" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) type rootRestartRequests struct { journal *release.RootRestarts source rootRestartRequestSource writes hostwrite.Config sys unitSystem checks rootRestartChecks getenv func(string) string optDir string } // runAdoptUnderRootRelease owns release exclusion for both call sites. func productionRestartRequestPass(ctx context.Context, store release.Store, writes hostwrite.Config, oldPass func() error) error { return restartRequestPass(legacyRootRestart, oldPass, func() error { census, err := rootparticipation.Open() if err != nil { return err } defer census.State.Close() state, err := rootstate.Open("/var/lib/carlos-claims/authority/fences", false, false) if err != nil { return err } defer state.Close() journal, err := rootstate.Open(release.RootRestartsDir, true, false) if err != nil { return err } defer journal.Close() sys := systemdUnits{} source := productionProofBindingSource(store, sys, release.OptDir()) source.cacheIndex = false checks := productionRootRestartChecks(sys, release.OptDir()) checks.host = source.forHost p := rootRestartRequests{journal: &release.RootRestarts{Store: journal}, source: rootRestartRequestSource{state: state, store: store, census: census.Census}, writes: writes, sys: sys, checks: checks, getenv: os.Getenv, optDir: release.OptDir()} return p.run(ctx) }) } func restartRequestPass(legacy func() (bool, error), oldPass, root func() error) error { old, err := legacy() if err != nil { return err } if !old { return root() } return oldPass() } func (p rootRestartRequests) run(ctx context.Context) error { apps, err := p.source.inventory(ctx) if err != nil { return err } retained, err := p.journal.Bindings() if err != nil { return err } census, err := p.source.census() if err != nil { return err } for _, b := range retained { if b.Box != census.Box { return os.ErrPermission } if _, ok := apps[b]; !ok { apps[b] = nil } } var failures []error for b, targets := range apps { body, id, readErr := p.source.store.Get(ctx, release.RestartGenKey(b.Account, b.App)) if errors.Is(readErr, os.ErrNotExist) { body, id, readErr = nil, release.RootRestartAbsent, nil } scopes, _, scopeErr := release.LoadRestartScopes(ctx, p.source.store, b.Account, b.App) in := release.RestartSnapshot{Complete: readErr == nil && scopeErr == nil, AppID: id, AppBody: body, Targets: targets} if scopes != nil { in.Scopes = *scopes } intent, err := p.journal.Observe(b, in) if err != nil { failures = append(failures, errors.Join(err, readErr, scopeErr)) continue } if intent == nil { continue } for _, progress := range intent.Targets { if progress.After != "" || progress.Failed { continue } if err := p.consume(ctx, b, intent.ID, progress); err != nil { failures = append(failures, err) } } if err := p.journal.Finish(b, intent.ID); err != nil { failures = append(failures, err) } } return errors.Join(failures...) } func (p rootRestartRequests) consume(ctx context.Context, b release.RestartBinding, id string, progress release.RestartProgress) error { target := progress.Target in, err := p.source.installation(ctx, target.Host) if err != nil { return err } if in.Empty || in.Box != b.Box || in.Account != b.Account || in.App != b.App { return os.ErrPermission } current, err := p.source.target(ctx, in) if err != nil { return err } if current != target { return os.ErrPermission } // A retained census never falls through the flag-off work provider. if !p.writes.Enabled { return os.ErrPermission } work, err := p.writes.Select(ctx, target.Host, "restart") if err != nil { return err } selected, err := hostbox.Selected(work) if err != nil { return err } if selected.Box != b.Box || selected.Captured != (hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}) { return os.ErrPermission } var before proofBinding check := func() error { fresh, err := p.source.target(ctx, in) if err != nil { return err } if fresh != target { return os.ErrPermission } observed, err := p.checks.host(ctx, target.Host) if err != nil { return err } if observed.Route.Account != b.Account || observed.Route.App != b.App || observed.Route.Unit != target.Unit || observed.RecordKey+"@"+observed.RecordETag != target.Source || observed.Invocation == "" || observed.PID <= 0 || !observed.authorizedRecovery() { return os.ErrPermission } props, err := p.sys.Show(target.Unit) if err != nil { return err } if props.ActiveState != "active" || props.MainPID != observed.PID || props.InvocationID != observed.Invocation { return os.ErrPermission } if before.Source != "" && (observed.Source != before.Source || observed.Invocation != before.Invocation || observed.Version != before.Version || observed.Digest != before.Digest) { return os.ErrPermission } before = observed return nil } previous := progress.Before if previous == "" { if err := work.With(ctx, hostwrite.Identity{}, func() error { if err := check(); err != nil { return err } previous = before.Invocation return p.journal.Begin(b, id, target, previous) }); err != nil { return err } } // Re-enter admission after the durable intent. A move that starts during // the write cannot turn a persisted intent into permission to restart. return work.With(ctx, hostwrite.Identity{}, func() error { if err := check(); err != nil { return err } if progress.Before == "" { if err := p.sys.TryRestart(target.Unit); err != nil { return err } } // An ambiguous invocation cannot be dispatched again, even after reboot. opts := settleOpts{Timeout: unitSettleTimeout(p.getenv), Poll: 500 * time.Millisecond, Hold: 5 * time.Second, RequireExe: true, WantDir: filepath.Dir(release.VersionPath(p.optDir, b.Account, before.Version, b.App)), Ready: p.checks.ready(before.Route), Now: p.checks.now, Sleep: p.checks.sleep} if err := settleUnit(p.sys, target.Unit, previous, opts); err != nil { if progress.Before == "" && restartEffectFailed(err) { props, showErr := p.sys.Show(target.Unit) failed, bindingErr := p.checks.host(ctx, target.Host) if showErr == nil && bindingErr == nil && props.ActiveState == "failed" && failed.Source == before.Source && failed.Version == before.Version && failed.Digest == before.Digest { return errors.Join(err, p.journal.Fail(b, id, target)) } } return err } after, err := p.checks.host(ctx, target.Host) if err != nil { return err } fresh, err := p.source.target(ctx, in) if err != nil { return err } if fresh != target || after.Source != before.Source || after.Version != before.Version || after.Digest != before.Digest || after.Invocation == previous || after.PID <= 0 { return os.ErrPermission } return p.journal.Complete(b, id, target, after.Invocation) }) } func logRestartRequestError(err error) { if err != nil { log.Printf("adopt: root restart requests: %v", err) } }