v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The unit's data files are stamped with the versions that wrote them; the capture keeps the definition the data belongs to; a restore never starts data under another version's definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's definition); every tier's time is its data's; the conversion-copy release needs a dump on the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept view joins the folder's owning group, language switch resyncs (R-691); a restore-generated login is not shown as the password (R-694). Red-proofs in felhom.eu/documentation/audits/version-travel-2026-09-26/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -91,6 +91,13 @@ type OffsiteReconstituteResult struct {
|
||||
// than reporting a bare success — a warning beside a success is read as a success, so the
|
||||
// difference has to survive into the message.
|
||||
Placement PlacementCheck
|
||||
// v0.275.0 (R-696, `07` §6.6) — the versions the snapshot's data belongs to (UnitRestoreResult's
|
||||
// fields, same meaning). VersionChanged: the app came back at the SNAPSHOT's version, its definition
|
||||
// written from the snapshot's unit, because the live one was another version.
|
||||
DataPins []string
|
||||
DataAt time.Time
|
||||
VersionChanged bool
|
||||
VersionsUnknown bool
|
||||
}
|
||||
|
||||
// fullPlaceCopier returns the FULL-restore file copier (nil seam → rsyncRestoreOverwrite).
|
||||
@@ -693,12 +700,50 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
return res, util.MsgError("err.backup.adatbazis_masolat_csonka_nem_indult", stack)
|
||||
}
|
||||
|
||||
// --- WHICH VERSION COMES BACK (v0.275.0, R-696, `07` §6.6) ---------------------------------
|
||||
// Until v0.275.0 this path never wrote the definition, so after any update the snapshot's data
|
||||
// (last night's version) was started by the app's NEW definition — for an engine step that is the
|
||||
// measured A1 failure (a PostgreSQL 16 datadir under 18: refused, the app left down). Now the
|
||||
// snapshot's data comes back with the definition it belongs to: when the snapshot records its data's
|
||||
// versions and they differ from what runs, the snapshot unit's definition is written into the stack
|
||||
// dir (and pinned) before anything is started, and the normal guarded update climbs from there. A
|
||||
// snapshot without that record restores as before, WARNed. Decided before the first mutation.
|
||||
scratchCompose := UnitComposeDir(scratchUnit)
|
||||
dataPins, verr := unitVersionCheck(stack, man, scratchCompose)
|
||||
if verr != nil {
|
||||
m.logger.Printf("[ERROR] [offbox] Restore REFUSED for %s: the snapshot's definition does not belong to its data — nothing was touched", stack)
|
||||
return res, verr
|
||||
}
|
||||
var snapEnv map[string]string
|
||||
defineFromSnapshot := false
|
||||
if dataPins == nil {
|
||||
res.VersionsUnknown = true
|
||||
m.logger.Printf("[WARN] [offbox] %s: snapshot %s does not record which versions wrote its data (taken before v0.275.0) — restoring into the app's current definition, as before", stack, id)
|
||||
} else {
|
||||
res.DataPins = dataPins
|
||||
res.DataAt, _ = man.Data.DataTime()
|
||||
if info, ok := m.stackProvider.GetStackRecoveryInfo(stack); ok && len(definitionPins(info)) > 0 && !samePins(definitionPins(info), dataPins) {
|
||||
env, _, eerr := m.unitRestoreEnv(stack, scratchCompose, man)
|
||||
if eerr != nil {
|
||||
return res, eerr
|
||||
}
|
||||
snapEnv, defineFromSnapshot, res.VersionChanged = env, true, true
|
||||
m.logger.Printf("[INFO] [offbox] %s: snapshot %s holds data of %v (written %s); the app runs %v — it comes back at the snapshot's version", stack, id, dataPins, man.Data.At, definitionPins(info))
|
||||
}
|
||||
}
|
||||
|
||||
// --- WHICH SERVICE HOLDS THE DATABASE (R-47) ------------------------------------------------
|
||||
// Read from the LIVE compose, not the scratch one: reconstitution never overwrites the stack dir,
|
||||
// so the live file is what `docker compose up` will actually act on. Resolved BEFORE the first
|
||||
// mutation so the refusal below costs nothing.
|
||||
// Read from the compose that will RUN: the live one, or — when the app comes back at the snapshot's
|
||||
// version — the snapshot unit's, which is written into the stack dir before the first start. Resolved
|
||||
// BEFORE the first mutation so the refusal below costs nothing.
|
||||
var dbServices []string
|
||||
if composePath, cOK := m.stackProvider.GetStackComposePath(stack); cOK && composePath != "" {
|
||||
if defineFromSnapshot {
|
||||
svcs, dsErr := DBServiceNames(filepath.Join(scratchCompose, "docker-compose.yml"))
|
||||
if dsErr != nil {
|
||||
m.logger.Printf("[WARN] [offbox] %s: could not read the snapshot's compose services: %v", stack, dsErr)
|
||||
}
|
||||
dbServices = svcs
|
||||
} else if composePath, cOK := m.stackProvider.GetStackComposePath(stack); cOK && composePath != "" {
|
||||
svcs, dsErr := DBServiceNames(composePath)
|
||||
if dsErr != nil {
|
||||
// "cannot tell" is not "no database" — leave dbServices empty and let the gate refuse.
|
||||
@@ -754,6 +799,16 @@ func (m *Manager) ReconstituteFromOffsite(ctx context.Context, stack string, ack
|
||||
if err := m.stackProvider.StopStack(stack); err != nil {
|
||||
m.logger.Printf("[WARN] [offbox] could not stop %s before reconstitution: %v (continuing)", stack, err)
|
||||
}
|
||||
// v0.275.0: the snapshot's own definition, written while nothing of the app's data has been touched
|
||||
// yet — a failure here restarts the app as it was.
|
||||
if defineFromSnapshot {
|
||||
if err := m.stackProvider.RecreateStackDefinitionFromUnit(stack, scratchCompose, snapEnv); err != nil {
|
||||
if sErr := restartStack(); sErr != nil {
|
||||
m.logger.Printf("[WARN] [offbox] %s: restart after a failed definition write also failed: %v", stack, sErr)
|
||||
}
|
||||
return res, fmt.Errorf("restoring %s: writing the snapshot's definition failed: %w", stack, err)
|
||||
}
|
||||
}
|
||||
copier := m.fullPlaceCopier()
|
||||
for _, pl := range placements {
|
||||
if pl.isUnit {
|
||||
|
||||
Reference in New Issue
Block a user