D47 / R-1269 (P2): a new install never writes into an earlier install's backup — every install has an identity (install_id), the unit records it, every unit writer asks first; another install's unit is set aside whole (backups/set-aside/<app>/<stamp>, a D25 kept copy, „Visszaállítás” + „Eltávolítás” on the restore page); a move that fails refuses the write. Red-proved. Commit only.
gates / gates (push) Successful in 1m33s

This commit is contained in:
2026-10-25 09:27:48 +01:00
parent e1613aea8d
commit 02daebe9c5
30 changed files with 1990 additions and 17 deletions
+60
View File
@@ -201,6 +201,21 @@ type AppConfig struct {
AfterSetup *AfterInstallRecord `yaml:"after_setup,omitempty" json:"after_setup,omitempty"`
// AfterUncleanBoot (R-1007) is what the template's after_unclean_boot last found and did (after_unclean_boot.go).
AfterUncleanBoot *AfterUncleanBootRecord `yaml:"after_unclean_boot,omitempty" json:"after_unclean_boot,omitempty"`
// InstallID (D47, operator ruling 2026-10-25; R-1269) names the install the app's DATA belongs to: minted at a
// fresh install and at an import, taken from the unit at a restore (the data is that unit's now), and carried by
// every load-then-save. The recovery unit records it (its app.yaml `install_id:`), and a backup of another install
// never writes into that unit (internal/backup/unit_owner.go). Absent = an app installed before D47 (minted at
// its first backup).
InstallID string `yaml:"install_id,omitempty" json:"install_id,omitempty"`
}
// NewInstallID mints an install identity (D47): 16 random hex characters. Never a secret, never derived from one.
func NewInstallID() string {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return fmt.Sprintf("t%015x", time.Now().UnixNano()&0xfffffffffffffff) // a clock fallback, still unique per box
}
return hex.EncodeToString(b)
}
// DefaultLoginRecord is app.yaml's `default_login:`.
@@ -541,6 +556,7 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
SetupGate: gate,
InstallHold: hold,
FamilyGate: family,
InstallID: NewInstallID(), // D47: a fresh install is a new install — its backups are its own
}
diskCfg := *appCfg
@@ -841,6 +857,18 @@ func (m *Manager) PersistUnitRedeployConfigGate(name string, env map[string]stri
cfg.RestoredLogins = restoredLoginFields(name, meta, prior, env)
priorRaw := LoadAppConfig(stackDir)
carryLifeRecords(m.logger, name, priorRaw, cfg)
// D47 (R-1269): the app now holds the data of the unit it was restored from, so it IS that unit's install — a
// later backup then writes into that unit, and a unit of the install it replaced is set aside, never rewritten.
// A unit that records no install (written before D47) keeps the app's own (carried above), else a new one.
switch {
case gate.InstallID != "":
if priorRaw != nil && priorRaw.InstallID != "" && priorRaw.InstallID != gate.InstallID {
m.logger.Printf("[INFO] [stacks] %s: the restored data belongs to install %s (was %s) — its backups follow it (D47)", name, gate.InstallID, priorRaw.InstallID)
}
cfg.InstallID = gate.InstallID
case cfg.InstallID == "":
cfg.InstallID = NewInstallID()
}
reopen, err := m.applyGateAtBackup(name, stack.ComposePath, env, &meta, cfg, priorRaw == nil, gate)
if err != nil {
return err
@@ -1623,6 +1651,38 @@ func restoredLoginFields(app string, meta Metadata, prior *AppConfig, env map[st
return out
}
// EnsureInstallID (D47) returns the app's install identity, minting and saving one (load-then-save, nothing else
// changes) for an app installed before D47. "" with an error when the record cannot be read or written — the caller
// (the backup) then refuses to decide whose unit it would write.
func (m *Manager) EnsureInstallID(name string) (string, error) {
stack, ok := m.GetStack(name)
if !ok {
return "", fmt.Errorf("stack %q not found", name)
}
stackDir := filepath.Dir(stack.ComposePath)
m.installIDMu.Lock()
defer m.installIDMu.Unlock()
cfg := LoadAppConfig(stackDir)
if cfg == nil {
return "", fmt.Errorf("app.yaml of %s not readable", name)
}
if cfg.InstallID != "" {
return cfg.InstallID, nil
}
meta := LoadMetadata(stackDir)
cfg.InstallID = NewInstallID()
if err := SaveAppConfig(stackDir, cfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
return "", fmt.Errorf("saving the install id of %s: %w", name, err)
}
m.mu.Lock()
if s, ok := m.stacks[name]; ok {
s.AppConfig = LoadAppConfig(stackDir)
}
m.mu.Unlock()
m.logger.Printf("[INFO] [stacks] %s: install id %s recorded (an app installed before D47)", name, cfg.InstallID)
return cfg.InstallID, nil
}
// SetStackSecretValues (R-1106) writes the named values into an installed app's app.yaml and NOTHING else: load the
// file as it is (records, pin, gates, locked fields all kept — the R-697 lesson: a non-install writer loads, then
// saves), replace the named env values, save with the sensitive fields encrypted. Used by the off-site restore to put