diff --git a/core/services/addons/manager.go b/core/services/addons/manager.go index c414c9398..954d99e63 100644 --- a/core/services/addons/manager.go +++ b/core/services/addons/manager.go @@ -364,6 +364,14 @@ func (m *Manager) Submit(ctx context.Context, actor contract.Actor, cmd Lifecycl if last, ok := lastReceipt(m.journal.Addons[cmd.AddonID]); ok && recovery.CheckCompatible(last, staged.Receipt()) != nil { return fail() } + if cmd.Action != "upgrade" { + // A reinstall has no running generation to back up, and + // the candidate would migrate the retained data when it + // starts. Only an upgrade takes a verified backup first. + if last, ok := lastReceipt(m.journal.Addons[cmd.AddonID]); ok && recovery.FormatChanged(last, staged.Receipt()) { + return fail() + } + } if cmd.Action == "upgrade" { return m.upgradeWithRecovery(ctx, cmd, op, &next, staged, fail) } diff --git a/core/services/addons/recovery/migration.go b/core/services/addons/recovery/migration.go index 53e3a2ab3..ee3aa8c26 100644 --- a/core/services/addons/recovery/migration.go +++ b/core/services/addons/recovery/migration.go @@ -35,6 +35,13 @@ func formatChanged(prev, next packages.PackageReceipt) bool { return next.Manifest.DataFormat.Max != prev.Manifest.DataFormat.Max } +// FormatChanged reports whether next would migrate the data that prev wrote. +// A caller that cannot take a backup first (a reinstall has no running +// generation to stop) must refuse such a package rather than migrate unbacked. +func FormatChanged(prev, next packages.PackageReceipt) bool { + return prev.Manifest.ID != "" && formatChanged(prev, next) +} + // migrationState reads the status file the add-on writes while it migrates. // The bool is false when it wrote none. func (r *Recovery) migrationState(addonID string) (string, string, bool) {