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
gates / gates (push) Successful in 1m33s
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user