D48: a restore downloads first — every restore path resolves and pulls every image of the definition that will run before the stop, under the image-work lock held until the restore returns; a failed pull refuses with the app untouched (a deleted version and a busy registry read differently), one operator line. Red-proved. Commit only.
gates / gates (push) Successful in 1m26s

This commit is contained in:
2026-10-25 09:38:28 +01:00
parent b680db85cb
commit 2d71270858
16 changed files with 404 additions and 3 deletions
@@ -946,6 +946,20 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
}
}
// D48: DOWNLOAD FIRST — every image of the definition that will run, before the stop, or a refusal with the app
// untouched. Held until this restore returns (R-863).
runCompose := filepath.Join(scratchCompose, "docker-compose.yml")
if !defineFromSnapshot {
if live, ok := m.stackProvider.GetStackComposePath(stack); ok && live != "" {
runCompose = live
}
}
releaseImages, pErr := m.prepareRestoreImages(stack, runCompose)
if pErr != nil {
return res, pErr
}
defer releaseImages()
// --- FILES ----------------------------------------------------------------------------------
// R-166: mark the stop→restore→start window BEFORE stopping. A controller killed anywhere inside
// it used to leave the app down with nothing on disk recording that it was owed a restart — and a