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:
@@ -169,6 +169,11 @@ type AppConfig struct {
|
||||
// ConversionCopy (v0.273.0, `09` §6.4 part 10) is the OLD datadir's copy kept after a successful
|
||||
// PostgreSQL major conversion, until a backup of the converted app is proven (ReleaseConversionCopies).
|
||||
ConversionCopy *ConversionCopy `yaml:"conversion_copy,omitempty" json:"conversion_copy,omitempty"`
|
||||
// RestoredLogins (v0.275.0, R-694) are the `type: password` fields whose stored value was GENERATED by a
|
||||
// restore (the unit never carries an admin login, D5, and the guest had none — a load of kept data, a
|
||||
// removed app, a rebuilt guest) while the app's own login came back with its data. The page then shows
|
||||
// no value for them and says the old password is the one that works (restoredLoginFields).
|
||||
RestoredLogins []string `yaml:"restored_logins,omitempty" json:"restored_logins,omitempty"`
|
||||
}
|
||||
|
||||
// InstalledImage is one compose service's observed image. See AppConfig.InstalledImages.
|
||||
@@ -684,6 +689,9 @@ func (m *Manager) PersistUnitRedeployConfig(name string, env map[string]string)
|
||||
stackDir := filepath.Dir(stack.ComposePath)
|
||||
meta := LoadMetadata(stackDir)
|
||||
|
||||
// R-694: which admin logins did the restore have to GENERATE? Exactly the `type: password` fields the
|
||||
// guest held no value for before this write (the unit never carries one) — read BEFORE it is replaced.
|
||||
prior := LoadAppConfigDecrypted(stackDir, m.encKey)
|
||||
cfg := &AppConfig{
|
||||
Deployed: true,
|
||||
DeployedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -694,6 +702,10 @@ func (m *Manager) PersistUnitRedeployConfig(name string, env map[string]string)
|
||||
cfg.LockedFields = append(cfg.LockedFields, f.EnvVar)
|
||||
}
|
||||
}
|
||||
cfg.RestoredLogins = restoredLoginFields(name, meta, prior, env)
|
||||
if len(cfg.RestoredLogins) > 0 {
|
||||
m.logger.Printf("[INFO] [stacks] %s: the restore generated %v — the app's own login came back with its data; the page will not show the new value as the password", name, cfg.RestoredLogins)
|
||||
}
|
||||
if err := SaveAppConfig(stackDir, cfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
|
||||
return fmt.Errorf("saving app config: %w", err)
|
||||
}
|
||||
@@ -1332,3 +1344,30 @@ func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLim
|
||||
}
|
||||
return nil, warning
|
||||
}
|
||||
|
||||
// loginAppliedEveryStart is the register of `type: password` fields whose app APPLIES the env value at
|
||||
// EVERY start, so a value generated at a restore IS the login afterwards (R-694, measured 2026-09-26 from
|
||||
// each image's entrypoint at the catalog's tag, `audits/version-travel-2026-09-26/D4/`): code-server's
|
||||
// s6 run script passes $PASSWORD to `code-server --auth password` on every start and stores none. The six
|
||||
// other apps with such a field (crafty-controller, gokapi, grafana, kimai, nextcloud, paperless-ngx) use it
|
||||
// only at first initialisation — the restored data's login wins. Code, not a catalog flag, like
|
||||
// nonPortableSecrets: it decides what a household is told about how to get into its own app.
|
||||
var loginAppliedEveryStart = map[string]map[string]bool{
|
||||
"code-server": {"PASSWORD": true},
|
||||
}
|
||||
|
||||
// restoredLoginFields names the `type: password` fields a restore GENERATED — no value in the guest's
|
||||
// app.yaml before, a value now — for an app whose login lives in its data. Pinned by TestR694_*.
|
||||
func restoredLoginFields(app string, meta Metadata, prior *AppConfig, env map[string]string) []string {
|
||||
var out []string
|
||||
for _, f := range meta.DeployFields {
|
||||
if f.Type != "password" || env[f.EnvVar] == "" || loginAppliedEveryStart[app][f.EnvVar] {
|
||||
continue
|
||||
}
|
||||
if prior != nil && prior.Env[f.EnvVar] != "" {
|
||||
continue // the guest kept the household's own value — not generated
|
||||
}
|
||||
out = append(out, f.EnvVar)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user