diff --git a/api/cli/cli.go b/api/cli/cli.go index 5f7062c21..d951d71b3 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -49,7 +49,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(), SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(), SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(), - Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(), + Rollback: kingpin.Flag("rollback", "Rollback the database to the previous backup").Bool(), SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(), AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), diff --git a/api/datastore/backup.go b/api/datastore/backup.go index a34e26786..c37684d2a 100644 --- a/api/datastore/backup.go +++ b/api/datastore/backup.go @@ -65,7 +65,7 @@ type BackupOptions struct { // - db rollback func getBackupRestoreOptions(backupDir string) *BackupOptions { return &BackupOptions{ - BackupDir: backupDir, //connection.commonBackupDir(), + BackupDir: backupDir, BackupFileName: beforePortainerVersionUpgradeBackup, } } @@ -76,12 +76,12 @@ func (store *Store) Backup(version *models.Version) (string, error) { return store.backupWithOptions(nil) } - return store.backupWithOptions(&BackupOptions{ - Version: version.SchemaVersion, - }) + backupOptions := getBackupRestoreOptions(store.commonBackupDir()) + backupOptions.Version = version.SchemaVersion + return store.backupWithOptions(backupOptions) } -func (store *Store) setupOptions(options *BackupOptions) *BackupOptions { +func (store *Store) setDefaultBackupOptions(options *BackupOptions) *BackupOptions { if options == nil { options = &BackupOptions{} } @@ -110,7 +110,7 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) { store.createBackupFolders() - options = store.setupOptions(options) + options = store.setDefaultBackupOptions(options) dbPath := store.databasePath() if err := store.Close(); err != nil { @@ -139,20 +139,18 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) { // - default: restore latest from current edition // - restore a specific func (store *Store) restoreWithOptions(options *BackupOptions) error { - options = store.setupOptions(options) + options = store.setDefaultBackupOptions(options) // Check if backup file exist before restoring _, err := os.Stat(options.BackupPath) if os.IsNotExist(err) { - log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to restore does not exist %s") - + log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to restore does not exist") return err } err = store.Close() if err != nil { log.Error().Err(err).Msg("error while closing store before restore") - return err } @@ -170,7 +168,7 @@ func (store *Store) restoreWithOptions(options *BackupOptions) error { func (store *Store) removeWithOptions(options *BackupOptions) error { log.Info().Msg("removing DB backup") - options = store.setupOptions(options) + options = store.setDefaultBackupOptions(options) _, err := os.Stat(options.BackupPath) if os.IsNotExist(err) { diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go index 7c424a9e4..066840f93 100644 --- a/api/datastore/migrate_data.go +++ b/api/datastore/migrate_data.go @@ -2,6 +2,7 @@ package datastore import ( "fmt" + "os" "runtime/debug" portainer "github.com/portainer/portainer/api" @@ -117,6 +118,11 @@ func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models return err } + // Special test code to simulate a failure (used by migrate_data_test.go). Do not remove... + if os.Getenv("PORTAINER_TEST_MIGRATE_FAIL") == "FAIL" { + panic("test migration failure") + } + err = store.VersionService.StoreIsUpdating(false) if err != nil { return errors.Wrap(err, "failed to update the store") diff --git a/api/datastore/migrate_data_test.go b/api/datastore/migrate_data_test.go index 389481b95..905320042 100644 --- a/api/datastore/migrate_data_test.go +++ b/api/datastore/migrate_data_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/boltdb" "github.com/portainer/portainer/api/database/models" @@ -55,111 +56,108 @@ func TestMigrateData(t *testing.T) { }) } - // t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) { - // newStore, store, teardown := MustNewTestStore(t, true, false) - // defer teardown() + t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) { + newStore, store := MustNewTestStore(t, true, false) - // if !newStore { - // t.Error("Expect a new DB") - // } + if !newStore { + t.Error("Expect a new DB") + } - // testVersion(store, portainer.APIVersion, t) - // store.Close() + testVersion(store, portainer.APIVersion, t) + store.Close() - // newStore, _ = store.Open() - // if newStore { - // t.Error("Expect store to NOT be new DB") - // } - // }) + newStore, _ = store.Open() + if newStore { + t.Error("Expect store to NOT be new DB") + } + }) - // tests := []struct { - // version string - // expectedVersion string - // }{ - // {version: "1.24.1", expectedVersion: portainer.APIVersion}, - // {version: "2.0.0", expectedVersion: portainer.APIVersion}, - // } - // for _, tc := range tests { - // _, store, teardown := MustNewTestStore(t, true, true) - // defer teardown() + tests := []struct { + version string + expectedVersion string + }{ + {version: "1.24.1", expectedVersion: portainer.APIVersion}, + {version: "2.0.0", expectedVersion: portainer.APIVersion}, + } + for _, tc := range tests { + _, store := MustNewTestStore(t, true, true) - // // Setup data - // v := models.Version{SchemaVersion: tc.version} - // store.VersionService.UpdateVersion(&v) + // Setup data + v := models.Version{SchemaVersion: tc.version, Edition: int(portainer.PortainerCE)} + store.VersionService.UpdateVersion(&v) - // // Required roles by migrations 22.2 - // store.RoleService.Create(&portainer.Role{ID: 1}) - // store.RoleService.Create(&portainer.Role{ID: 2}) - // store.RoleService.Create(&portainer.Role{ID: 3}) - // store.RoleService.Create(&portainer.Role{ID: 4}) + // Required roles by migrations 22.2 + store.RoleService.Create(&portainer.Role{ID: 1}) + store.RoleService.Create(&portainer.Role{ID: 2}) + store.RoleService.Create(&portainer.Role{ID: 3}) + store.RoleService.Create(&portainer.Role{ID: 4}) - // t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) { - // store.MigrateData() - // testVersion(store, tc.expectedVersion, t) - // }) + t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) { + store.MigrateData() + testVersion(store, tc.expectedVersion, t) + }) - // t.Run(fmt.Sprintf("Restoring DB after migrateData for version %s", tc.version), func(t *testing.T) { - // store.Rollback(true) - // store.Open() - // testVersion(store, tc.version, t) - // }) - // } + t.Run(fmt.Sprintf("Restoring DB after migrateData for version %s", tc.version), func(t *testing.T) { + store.Rollback(true) + store.Open() + testVersion(store, tc.version, t) + }) + } - // t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) { - // _, store, teardown := MustNewTestStore(t, false, true) - // defer teardown() + t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) { + _, store := MustNewTestStore(t, false, false) - // v := models.Version{SchemaVersion: "1.24.1"} - // store.VersionService.UpdateVersion(&v) + v := models.Version{SchemaVersion: "1.24.1", Edition: int(portainer.PortainerCE)} + store.VersionService.UpdateVersion(&v) - // store.MigrateData() + store.MigrateData() - // testVersion(store, v.SchemaVersion, t) - // }) + testVersion(store, v.SchemaVersion, t) + }) - // t.Run("MigrateData should create backup file upon update", func(t *testing.T) { - // _, store, teardown := MustNewTestStore(t, false, true) - // defer teardown() + t.Run("MigrateData should create backup file upon update", func(t *testing.T) { + _, store := MustNewTestStore(t, false, false) - // v := models.Version{SchemaVersion: "0.0.0"} - // store.VersionService.UpdateVersion(&v) + v := models.Version{SchemaVersion: "0.0.0", Edition: int(portainer.PortainerCE)} + store.VersionService.UpdateVersion(&v) - // store.MigrateData() + store.MigrateData() - // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) + options := store.setDefaultBackupOptions(getBackupRestoreOptions(store.commonBackupDir())) - // if !isFileExist(options.BackupPath) { - // t.Errorf("Backup file should exist; file=%s", options.BackupPath) - // } - // }) + if !isFileExist(options.BackupPath) { + t.Errorf("Backup file should exist; file=%s", options.BackupPath) + } + }) - // t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) { - // _, store, teardown := MustNewTestStore(t, false, true) - // defer teardown() + t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) { + _, store := MustNewTestStore(t, false, false) - // store.VersionService.StoreIsUpdating(true) + store.VersionService.StoreIsUpdating(true) - // store.MigrateData() + store.MigrateData() - // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) + options := store.setDefaultBackupOptions(getBackupRestoreOptions(store.commonBackupDir())) - // if isFileExist(options.BackupPath) { - // t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath) - // } - // }) + if isFileExist(options.BackupPath) { + t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath) + } + }) - // t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) { - // _, store, teardown := MustNewTestStore(t, false, true) - // defer teardown() + t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) { + os.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL") - // store.MigrateData() + version := "2.15" + _, store := MustNewTestStore(t, true, false) + store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)}) + err := store.MigrateData() + if err == nil { + t.Errorf("Expect migration to fail") + } - // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) - - // if isFileExist(options.BackupPath) { - // t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath) - // } - // }) + store.Open() + testVersion(store, version, t) + }) } func Test_getBackupRestoreOptions(t *testing.T) {