test: cover the legacy -v migration with go and bats tests

Adds plugins/storage/migrate_test.go exercising migrateApp and MigrateLegacyMounts: single deploy phase, cross-phase grouping into one attachment, :ro and free-form volume options preserved on the attachment, idempotency on a second pass, the per-app flag-file fast path that skips already-migrated apps, and the name-collision refusal that aborts before draining. tests/unit/storage.bats gains an end-to-end case that stages a -v line via docker-options:add, runs storage:migrate, and asserts the synthesized colon form shows up in storage:list, the legacy-<hash> entry shows up in storage:list-entries, and the original -v line is gone from docker-options:report on both phases. To make that test (and operators with restored backups) practical, storage gains a storage:migrate <app>|--all subcommand backed by a new MigrateApp helper that skips the per-app flag file so a re-run actually re-scans.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-30 02:37:01 -04:00
parent e9a53ac82d
commit f0d088bc1d
8 changed files with 446 additions and 1 deletions

View File

@@ -25,6 +25,21 @@ func migrationFlagFile(appName string) string {
return filepath.Join(migrationFlagDir(), appName)
}
// MigrateApp re-runs the legacy migration for a single app, ignoring
// the per-app flag file so an operator can force-rescan an app whose
// docker-options state changed after the bulk install-time pass.
// Used by `dokku storage:migrate <app>`.
func MigrateApp(appName string) error {
if err := os.MkdirAll(migrationFlagDir(), 0755); err != nil {
return fmt.Errorf("unable to create migration flag dir: %w", err)
}
_ = os.Remove(migrationFlagFile(appName))
if err := migrateApp(appName); err != nil {
return fmt.Errorf("storage migration failed for app %q: %w", appName, err)
}
return nil
}
// MigrateLegacyMounts walks every app and converts its legacy `-v`
// docker-options entries into named storage entries plus attachments.
// Idempotent: per-app flag files short-circuit re-runs.