v0.330.0: „Végleges leszerelés — mégis" asks the agent's disk list first and starts again the apps it stopped when the agent refuses (R-1144)
gates / gates (push) Successful in 52s
gates / gates (push) Successful in 52s
Red-proofed twice. MinAgent 0.158.1.
This commit is contained in:
@@ -547,28 +547,28 @@ func (s *Server) handleStorageDecommission(w http.ResponseWriter, r *http.Reques
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, s.msg(r, "disk.err.name_mismatch"), nil)
|
||||
return
|
||||
}
|
||||
// Stop the apps that live on this drive — but KEEP their HDD_PATH so the dashboard can name the
|
||||
// drive in the "missing storage" indicator until the customer re-enrolls or migrates.
|
||||
var stopped []string
|
||||
for _, st := range s.stackMgr.GetStacks() {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
if cfg := s.stackMgr.LoadAppConfigByName(st.Name); cfg != nil && cfg.Env["HDD_PATH"] == req.Where {
|
||||
if err := s.stackMgr.StopStack(st.Name); err != nil {
|
||||
s.logger.Printf("[WARN] [web] decommission: stop %s failed: %v", st.Name, err)
|
||||
}
|
||||
stopped = append(stopped, st.Meta.DisplayName)
|
||||
}
|
||||
agent, aerr := s.agentClient()
|
||||
if aerr != nil {
|
||||
writeDiskJSON(w, http.StatusBadGateway, false, s.errText(r, aerr), nil)
|
||||
return
|
||||
}
|
||||
if err := s.finalizeDecommission(r.Context(), req.Where, ""); err != nil {
|
||||
// The M1 policy refusal is a 409 (CONFLICT with current state), not a gateway
|
||||
// failure — and 409 passes the CF edge with its JSON body intact.
|
||||
stopped, restartedAll, err := s.decommissionAnyway(r.Context(), agent, s.stackMgr, req.Where)
|
||||
if err != nil {
|
||||
// The policy refusals (M1, the agent's 403/409) are 409s (CONFLICT with current state), not gateway
|
||||
// failures — and a 409 passes the CF edge with its JSON body intact.
|
||||
status := http.StatusBadGateway
|
||||
if isDecommissionPolicyRefusal(err) {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
writeDiskJSON(w, status, false, s.errText(r, err), nil)
|
||||
msg := s.errText(r, err)
|
||||
if len(stopped) > 0 && driveUntouched(err) {
|
||||
key := "disk.decommission.apps_restarted"
|
||||
if !restartedAll {
|
||||
key = "disk.decommission.apps_not_all_restarted"
|
||||
}
|
||||
msg += " " + s.msg(r, key)
|
||||
}
|
||||
writeDiskJSON(w, status, false, msg, map[string]any{"stopped_apps": stopped, "restarted": restartedAll})
|
||||
return
|
||||
}
|
||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"decommissioned": true, "where": req.Where, "stopped_apps": stopped})
|
||||
@@ -590,6 +590,99 @@ var (
|
||||
errDecommissionBackupTarget = util.MsgError("err.web.decommission_backup_target")
|
||||
)
|
||||
|
||||
// errDecommissionNotDone marks a finalize failure in which the agent did NOT decommission the drive (its call failed):
|
||||
// like a policy refusal, the drive and the registry are as they were.
|
||||
var errDecommissionNotDone = errors.New("the drive was not decommissioned")
|
||||
|
||||
// driveUntouched: the finalize failed BEFORE the drive was let go — a policy refusal or a failed agent call — so the
|
||||
// drive still serves and the apps stopped for it can run again. NOT true when the agent let go and only the registry
|
||||
// mark failed: then the drive is gone and its apps must stay stopped.
|
||||
func driveUntouched(err error) bool {
|
||||
return isDecommissionPolicyRefusal(err) || errors.Is(err, errDecommissionNotDone)
|
||||
}
|
||||
|
||||
// decomStacks is the slice of the stack manager „Végleges leszerelés — mégis" uses (a seam: tests never reach docker).
|
||||
type decomStacks interface {
|
||||
GetStacks() []stacks.Stack
|
||||
LoadAppConfigByName(name string) *stacks.AppConfig
|
||||
StopStack(name string) error
|
||||
StartStack(name string) error
|
||||
}
|
||||
|
||||
// wasUp: the app was running (in any of its running shapes) before the press.
|
||||
func wasUp(st stacks.ContainerState) bool {
|
||||
switch st {
|
||||
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateDegraded, stacks.StateRestarting:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// decommissionAnyway is mode „anyway": ask the agent's verdict BEFORE stopping anything where the agent already says it
|
||||
// (its own disk list: a role other than user-data → 403, the whole-guest backup target → 409), then stop the drive's
|
||||
// apps, then finalize. R-1144 (2026-10-18): when the finalize leaves the drive untouched (a policy refusal, a failed
|
||||
// agent call), EXACTLY the apps that were running before the press are started again. Returns the stopped apps'
|
||||
// display names and whether every app that was running is running again. Pinned by TestR1144_*.
|
||||
func (s *Server) decommissionAnyway(ctx context.Context, agent diskAgent, sm decomStacks, where string) (stopped []string, restartedAll bool, err error) {
|
||||
if perr := s.decommissionPrecheck(ctx, agent, where); perr != nil {
|
||||
return nil, true, perr
|
||||
}
|
||||
// Stop the apps that live on this drive — but KEEP their HDD_PATH so the dashboard can name the drive in the
|
||||
// "missing storage" indicator until the customer re-enrolls or migrates.
|
||||
var wereUp []string
|
||||
for _, st := range sm.GetStacks() {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
if cfg := sm.LoadAppConfigByName(st.Name); cfg != nil && cfg.Env["HDD_PATH"] == where {
|
||||
up := wasUp(st.State)
|
||||
if serr := sm.StopStack(st.Name); serr != nil {
|
||||
s.logger.Printf("[WARN] [web] decommission: stop %s failed: %v", st.Name, serr)
|
||||
}
|
||||
if up {
|
||||
wereUp = append(wereUp, st.Name)
|
||||
}
|
||||
stopped = append(stopped, st.Meta.DisplayName)
|
||||
}
|
||||
}
|
||||
err = s.finalizeDecommissionWith(ctx, agent, where, "")
|
||||
if err == nil || !driveUntouched(err) {
|
||||
return stopped, err == nil, err
|
||||
}
|
||||
restartedAll = true
|
||||
for _, name := range wereUp {
|
||||
if serr := sm.StartStack(name); serr != nil {
|
||||
restartedAll = false
|
||||
s.logger.Printf("[ERROR] [web] decommission of %s refused, and %s did not start again: %v", where, name, serr)
|
||||
}
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] decommission of %s did not happen — %d app(s) that were running started again (all=%v)", where, len(wereUp), restartedAll)
|
||||
return stopped, restartedAll, err
|
||||
}
|
||||
|
||||
// decommissionPrecheck answers the agent's two policy refusals from its own disk list, before anything is stopped.
|
||||
// The agent stays the authority (finalize asks it again); a list it cannot give, or a drive not in it, is no verdict.
|
||||
func (s *Server) decommissionPrecheck(ctx context.Context, agent diskAgent, where string) error {
|
||||
resp, err := agent.Disks(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
raw := agentWhere(where)
|
||||
for _, d := range resp.Disks {
|
||||
if d.MountPath != raw && d.GuestPath != where {
|
||||
continue
|
||||
}
|
||||
if d.BackupTarget {
|
||||
return errDecommissionBackupTarget
|
||||
}
|
||||
if d.Role != "" && d.Role != "user-data" {
|
||||
return errDecommissionProtected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDecommissionPolicyRefusal: a refusal the household can act on (409 with its JSON), not a gateway failure.
|
||||
func isDecommissionPolicyRefusal(err error) bool {
|
||||
return errors.Is(err, errLastUsableDrive) || errors.Is(err, errDecommissionProtected) ||
|
||||
@@ -632,7 +725,7 @@ func (s *Server) finalizeDecommissionWith(ctx context.Context, agent diskAgent,
|
||||
}
|
||||
return errDecommissionProtected
|
||||
}
|
||||
return util.MsgError("err.web.a_meghajto_leszerelese_sikertelen", err)
|
||||
return util.MsgErrorf(errDecommissionNotDone, "err.web.a_meghajto_leszerelese_sikertelen", err)
|
||||
}
|
||||
if err := s.settings.SetDecommissioned(where, migratedTo); err != nil {
|
||||
// The agent has let go of the drive; only the dashboard's record failed. Said loudly — the next
|
||||
|
||||
Reference in New Issue
Block a user