Files
felhom-controller/controller/internal/backup/hold_clear.go
T
admin 5b3b541602
gates / gates (push) Successful in 31s
clear_hold (D64 = B) + the box command through the running controller (R-1343), R-1340, R-1341 — tests red first; commit only
- clear_hold: a fifth verb in the closed operator-action list; lifts a hold of any reason in the RUNNING
  controller (memory + settings.json together), forgets the update's held flag, drops a waiting put-back
  from the app-stop marker (copy kept). Refused on a bad name or no hold. No new event type.
- --clear-restore-hold asks the running controller over <data dir>/ctl/hold.sock (unix, 0600, peer uid
  checked, no TCP); edits settings.json only when no controller answers, and says so (in-guest restart).
- R-1340: the operator's HELD STOPPED mail is built from the hold record (restore kind, mixed cause,
  failed put-back step, database replay or not, kept copy) — never the option-C database story by default.
- R-1341: the OOM WARN line once per new kill (container, start time, kill count); event unchanged.
2026-10-29 12:49:46 +01:00

67 lines
3.2 KiB
Go

package backup
import (
"errors"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// ── D64 = B (operator ruling 2026-10-29 12:10) + R-1343: the operator clears a hold IN THE RUNNING CONTROLLER ──────
//
// One door for both of the operator's routes — the hub's „Clear hold" (operator action clear_hold) and the box command
// `--clear-restore-hold`, which now asks the running controller over its local socket (cmd/controller/holdctl.go).
//
// WHY IN THIS PROCESS (R-1343, measured 2026-10-29 on Tester 1): the CLI used to run as a SECOND process that edited
// settings.json while the running controller kept the hold in its own in-memory Settings — and that controller's next
// save (after a hub report) wrote the hold back. Clearing through the running Settings removes the hold from memory
// AND saves the file at once, so no later save of any component can bring it back.
// Pinned by TestD64_ClearHoldStaysGoneAfterALaterSave (cmd/controller).
//
// EVERY REASON (D64 = B): the hold's reason does not matter — R-379 now reads „a person — on the box, or the operator
// on the hub". One reason needs a second act: a put-back WAITING for its drive (HoldReasonPutBackPending) is also
// recorded in the app-stop marker, and a controller start with the drive bound would pour that old copy back over
// whatever the app wrote after the clear. So the clear also drops the app from the marker; the copy stays on disk
// (nothing is deleted). It runs under resumeMu, so it can never interleave with a put-back of the same app.
// Pinned by TestD64_ClearPutBackPendingDropsTheMarkerEntry.
// ErrHoldBadStack: the argument is not a valid stack name. ErrNotHeld: the app has no hold. Both are refusals —
// nothing was changed.
var (
ErrHoldBadStack = errors.New("not a valid app name")
ErrNotHeld = errors.New("this app has no hold")
)
// IsValidHoldStack reports whether s may name an app for the hold commands (the backup package's strict
// isSafeStackName: letters, digits, '-' and '_', at most 64).
func IsValidHoldStack(s string) bool { return isSafeStackName(s) }
// ClearHoldByOperator lifts stack's hold, whatever its reason, in the hold store the running controller uses, and
// returns the hold it lifted. A waiting put-back is dropped from the app-stop marker too (see above). Refusals
// (ErrHoldBadStack, ErrNotHeld) change nothing.
func (g *AppStopGuard) ClearHoldByOperator(stack string) (settings.RestoreHold, error) {
if !isSafeStackName(stack) {
return settings.RestoreHold{}, ErrHoldBadStack
}
if g == nil || g.holds == nil {
return settings.RestoreHold{}, errors.New("no hold store is wired on this box")
}
resumeMu.Lock()
defer resumeMu.Unlock()
h, ok := g.holds.GetRestoreHold(stack)
if !ok {
return settings.RestoreHold{}, ErrNotHeld
}
cleared, err := g.holds.ClearRestoreHold(stack)
if err != nil {
return h, err
}
if !cleared {
return settings.RestoreHold{}, ErrNotHeld
}
if h.Reason == settings.HoldReasonPutBackPending {
g.dropFromMarker(stack)
g.logger.Printf("[WARN] [appstop] %s: its waiting put-back is DROPPED with the hold — the copy stays on disk at %s and is never poured back by itself", stack, h.SafetyDump)
}
return h, nil
}