From f58aa8cd5bc917523275a7c506f34fe626fa6f91 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Thu, 7 Dec 2023 16:42:41 +1300 Subject: [PATCH] fix(rollback): reversed rollback code from 2.19.4 [EE-6435] (#10787) * Revert "fix(rollback): reimplement rollback feature [EE-6367] (#10720)" This reverts commit 93124f75cfe123f61ef328a4113526389c3af566. * Revert "fix(backups): fix rollback feature [EE-6367] (#10691) (#10703)" This reverts commit 0fce4c98a07a93a7b02313150c3dab1ce548cbe8. --- api/cli/cli.go | 2 +- api/cli/confirm.go | 2 +- api/datastore/backup.go | 228 +++++++++++++++++------ api/datastore/backup_test.go | 107 +++++++---- api/datastore/datastore.go | 6 - api/datastore/helpers_test.go | 68 ------- api/datastore/migrate_data.go | 20 +- api/datastore/migrate_data_test.go | 286 +++++++++++++++-------------- api/portainer.go | 14 -- 9 files changed, 393 insertions(+), 340 deletions(-) delete mode 100644 api/datastore/helpers_test.go diff --git a/api/cli/cli.go b/api/cli/cli.go index d951d71b3..5f7062c21 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 to the previous backup").Bool(), + Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").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/cli/confirm.go b/api/cli/confirm.go index bba2c12fe..ec1076057 100644 --- a/api/cli/confirm.go +++ b/api/cli/confirm.go @@ -9,7 +9,7 @@ import ( // Confirm starts a rollback db cli application func Confirm(message string) (bool, error) { - fmt.Printf("%s [y/N] ", message) + fmt.Printf("%s [y/N]", message) reader := bufio.NewReader(os.Stdin) diff --git a/api/datastore/backup.go b/api/datastore/backup.go index df0ad9583..a34e26786 100644 --- a/api/datastore/backup.go +++ b/api/datastore/backup.go @@ -1,77 +1,189 @@ package datastore import ( + "fmt" "os" "path" + "time" + "github.com/portainer/portainer/api/database/models" "github.com/rs/zerolog/log" ) -func (store *Store) Backup() (string, error) { - if err := store.createBackupPath(); err != nil { - return "", err - } - - backupFilename := store.backupFilename() - log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("Backing up database") - err := store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true) - if err != nil { - log.Warn().Err(err).Msg("failed to create backup file") - return "", err - } - - return backupFilename, nil +var backupDefaults = struct { + backupDir string + commonDir string +}{ + "backups", + "common", } -func (store *Store) Restore() error { - backupFilename := store.backupFilename() - return store.RestoreFromFile(backupFilename) -} +// +// Backup Helpers +// -func (store *Store) RestoreFromFile(backupFilename string) error { - if exists, _ := store.fileService.FileExists(backupFilename); !exists { - log.Error().Str("backupFilename", backupFilename).Msg("backup file does not exist") - return os.ErrNotExist - } - - if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil { - log.Error().Err(err).Msg("error while restoring backup.") - return err - } - - log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("database restored") - - // determine the db version - store.Open() - version, err := store.VersionService.Version() - - edition := "CE" - if version.Edition == 2 { - edition = "EE" - } - - if err == nil { - log.Info().Str("version", version.SchemaVersion).Msgf("Restored database version: Portainer %s %s", edition, version.SchemaVersion) - } - - return nil -} - -func (store *Store) createBackupPath() error { - backupDir := path.Join(store.connection.GetStorePath(), "backups") - if exists, _ := store.fileService.FileExists(backupDir); !exists { - if err := os.MkdirAll(backupDir, 0700); err != nil { - log.Error().Err(err).Msg("error while creating backup folder") - return err +// createBackupFolders create initial folders for backups +func (store *Store) createBackupFolders() { + // create common dir + commonDir := store.commonBackupDir() + if exists, _ := store.fileService.FileExists(commonDir); !exists { + if err := os.MkdirAll(commonDir, 0700); err != nil { + log.Error().Err(err).Msg("error while creating common backup folder") } } - return nil -} - -func (store *Store) backupFilename() string { - return path.Join(store.connection.GetStorePath(), "backups", store.connection.GetDatabaseFileName()+".bak") } func (store *Store) databasePath() string { return store.connection.GetDatabaseFilePath() } + +func (store *Store) commonBackupDir() string { + return path.Join(store.connection.GetStorePath(), backupDefaults.backupDir, backupDefaults.commonDir) +} + +func (store *Store) copyDBFile(from string, to string) error { + log.Info().Str("from", from).Str("to", to).Msg("copying DB file") + + err := store.fileService.Copy(from, to, true) + if err != nil { + log.Error().Err(err).Msg("failed") + } + + return err +} + +// BackupOptions provide a helper to inject backup options +type BackupOptions struct { + Version string + BackupDir string + BackupFileName string + BackupPath string +} + +// getBackupRestoreOptions returns options to store db at common backup dir location; used by: +// - db backup prior to version upgrade +// - db rollback +func getBackupRestoreOptions(backupDir string) *BackupOptions { + return &BackupOptions{ + BackupDir: backupDir, //connection.commonBackupDir(), + BackupFileName: beforePortainerVersionUpgradeBackup, + } +} + +// Backup current database with default options +func (store *Store) Backup(version *models.Version) (string, error) { + if version == nil { + return store.backupWithOptions(nil) + } + + return store.backupWithOptions(&BackupOptions{ + Version: version.SchemaVersion, + }) +} + +func (store *Store) setupOptions(options *BackupOptions) *BackupOptions { + if options == nil { + options = &BackupOptions{} + } + if options.Version == "" { + v, err := store.VersionService.Version() + if err != nil { + options.Version = "" + } + options.Version = v.SchemaVersion + } + if options.BackupDir == "" { + options.BackupDir = store.commonBackupDir() + } + if options.BackupFileName == "" { + options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), options.Version, time.Now().Format("20060102150405")) + } + if options.BackupPath == "" { + options.BackupPath = path.Join(options.BackupDir, options.BackupFileName) + } + return options +} + +// BackupWithOptions backup current database with options +func (store *Store) backupWithOptions(options *BackupOptions) (string, error) { + log.Info().Msg("creating DB backup") + + store.createBackupFolders() + + options = store.setupOptions(options) + dbPath := store.databasePath() + + if err := store.Close(); err != nil { + return options.BackupPath, fmt.Errorf( + "error closing datastore before creating backup: %w", + err, + ) + } + + if err := store.copyDBFile(dbPath, options.BackupPath); err != nil { + return options.BackupPath, err + } + + if _, err := store.Open(); err != nil { + return options.BackupPath, fmt.Errorf( + "error opening datastore after creating backup: %w", + err, + ) + } + + return options.BackupPath, nil +} + +// RestoreWithOptions previously saved backup for the current Edition with options +// Restore strategies: +// - default: restore latest from current edition +// - restore a specific +func (store *Store) restoreWithOptions(options *BackupOptions) error { + options = store.setupOptions(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") + + return err + } + + err = store.Close() + if err != nil { + log.Error().Err(err).Msg("error while closing store before restore") + + return err + } + + log.Info().Msg("restoring DB backup") + err = store.copyDBFile(options.BackupPath, store.databasePath()) + if err != nil { + return err + } + + _, err = store.Open() + return err +} + +// RemoveWithOptions removes backup database based on supplied options +func (store *Store) removeWithOptions(options *BackupOptions) error { + log.Info().Msg("removing DB backup") + + options = store.setupOptions(options) + _, err := os.Stat(options.BackupPath) + + if os.IsNotExist(err) { + log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist") + return err + } + + log.Info().Str("path", options.BackupPath).Msg("removing DB file") + err = os.Remove(options.BackupPath) + if err != nil { + log.Error().Err(err).Msg("failed") + return err + } + + return nil +} diff --git a/api/datastore/backup_test.go b/api/datastore/backup_test.go index 0a82a33db..f93103859 100644 --- a/api/datastore/backup_test.go +++ b/api/datastore/backup_test.go @@ -2,79 +2,106 @@ package datastore import ( "fmt" + "os" + "path" "testing" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/models" - - "github.com/rs/zerolog/log" ) +func TestCreateBackupFolders(t *testing.T) { + _, store := MustNewTestStore(t, true, true) + + connection := store.GetConnection() + backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir) + + if isFileExist(backupPath) { + t.Error("Expect backups folder to not exist") + } + + store.createBackupFolders() + if !isFileExist(backupPath) { + t.Error("Expect backups folder to exist") + } +} + func TestStoreCreation(t *testing.T) { _, store := MustNewTestStore(t, true, true) if store == nil { - t.Fatal("Expect to create a store") + t.Error("Expect to create a store") } - v, err := store.VersionService.Version() - if err != nil { - log.Fatal().Err(err).Msg("") - } - - if portainer.SoftwareEdition(v.Edition) != portainer.PortainerCE { + if store.CheckCurrentEdition() != nil { t.Error("Expect to get CE Edition") } - - if v.SchemaVersion != portainer.APIVersion { - t.Error("Expect to get APIVersion") - } } func TestBackup(t *testing.T) { _, store := MustNewTestStore(t, true, true) - backupFileName := store.backupFilename() - t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) { + connection := store.GetConnection() + + t.Run("Backup should create default db backup", func(t *testing.T) { v := models.Version{ - Edition: int(portainer.PortainerCE), SchemaVersion: portainer.APIVersion, } store.VersionService.UpdateVersion(&v) - store.Backup() + store.backupWithOptions(nil) + backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion)) + if !isFileExist(backupFileName) { + t.Errorf("Expect backup file to be created %s", backupFileName) + } + }) + + t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) { + store.backupWithOptions(&BackupOptions{ + BackupFileName: beforePortainerVersionUpgradeBackup, + BackupDir: store.commonBackupDir(), + }) + backupFileName := path.Join(connection.GetStorePath(), "backups", "common", beforePortainerVersionUpgradeBackup) if !isFileExist(backupFileName) { t.Errorf("Expect backup file to be created %s", backupFileName) } }) } -func TestRestore(t *testing.T) { - _, store := MustNewTestStore(t, true, false) +func TestRemoveWithOptions(t *testing.T) { + _, store := MustNewTestStore(t, true, true) - t.Run(fmt.Sprintf("Basic Restore"), func(t *testing.T) { - // override and set initial db version and edition - updateEdition(store, portainer.PortainerCE) - updateVersion(store, "2.4") + t.Run("successfully removes file if existent", func(t *testing.T) { + store.createBackupFolders() + options := &BackupOptions{ + BackupDir: store.commonBackupDir(), + BackupFileName: "test.txt", + } - store.Backup() - updateVersion(store, "2.16") - testVersion(store, "2.16", t) - store.Restore() + filePath := path.Join(options.BackupDir, options.BackupFileName) + f, err := os.Create(filePath) + if err != nil { + t.Fatalf("file should be created; err=%s", err) + } + f.Close() - // check if the restore is successful and the version is correct - testVersion(store, "2.4", t) + err = store.removeWithOptions(options) + if err != nil { + t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err) + } + + if isFileExist(f.Name()) { + t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name()) + } }) - t.Run(fmt.Sprintf("Basic Restore After Multiple Backups"), func(t *testing.T) { - // override and set initial db version and edition - updateEdition(store, portainer.PortainerCE) - updateVersion(store, "2.4") - store.Backup() - updateVersion(store, "2.14") - updateVersion(store, "2.16") - testVersion(store, "2.16", t) - store.Restore() + t.Run("fails to removes file if non-existent", func(t *testing.T) { + options := &BackupOptions{ + BackupDir: store.commonBackupDir(), + BackupFileName: "test.txt", + } - // check if the restore is successful and the version is correct - testVersion(store, "2.4", t) + err := store.removeWithOptions(options) + if err == nil { + t.Error("RemoveWithOptions should fail for non-existent file") + } }) } diff --git a/api/datastore/datastore.go b/api/datastore/datastore.go index ceebbd0b0..9703cde8e 100644 --- a/api/datastore/datastore.go +++ b/api/datastore/datastore.go @@ -31,14 +31,8 @@ func (store *Store) Open() (newStore bool, err error) { } if encryptionReq { - backupFilename, err := store.Backup() - if err != nil { - return false, fmt.Errorf("failed to backup database prior to encrypting: %w", err) - } - err = store.encryptDB() if err != nil { - store.RestoreFromFile(backupFilename) // restore from backup if encryption fails return false, err } } diff --git a/api/datastore/helpers_test.go b/api/datastore/helpers_test.go deleted file mode 100644 index c35d76b6d..000000000 --- a/api/datastore/helpers_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package datastore - -import ( - "path/filepath" - "testing" - - portainer "github.com/portainer/portainer/api" - - "github.com/rs/zerolog/log" -) - -// isFileExist is helper function to check for file existence -func isFileExist(path string) bool { - matches, err := filepath.Glob(path) - if err != nil { - return false - } - return len(matches) > 0 -} - -func updateVersion(store *Store, v string) { - version, err := store.VersionService.Version() - if err != nil { - log.Fatal().Err(err).Msg("") - } - - version.SchemaVersion = v - - err = store.VersionService.UpdateVersion(version) - if err != nil { - log.Fatal().Err(err).Msg("") - } -} - -func updateEdition(store *Store, edition portainer.SoftwareEdition) { - version, err := store.VersionService.Version() - if err != nil { - log.Fatal().Err(err).Msg("") - } - - version.Edition = int(edition) - - err = store.VersionService.UpdateVersion(version) - if err != nil { - log.Fatal().Err(err).Msg("") - } -} - -// testVersion is a helper which tests current store version against wanted version -func testVersion(store *Store, versionWant string, t *testing.T) { - v, err := store.VersionService.Version() - if err != nil { - log.Fatal().Err(err).Msg("") - } - if v.SchemaVersion != versionWant { - t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion) - } -} - -func testEdition(store *Store, editionWant portainer.SoftwareEdition, t *testing.T) { - v, err := store.VersionService.Version() - if err != nil { - log.Fatal().Err(err).Msg("") - } - if portainer.SoftwareEdition(v.Edition) != editionWant { - t.Errorf("Expect store edition to be %s but was %s", editionWant.GetEditionLabel(), portainer.SoftwareEdition(v.Edition).GetEditionLabel()) - } -} diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go index 3cbfe2521..7c424a9e4 100644 --- a/api/datastore/migrate_data.go +++ b/api/datastore/migrate_data.go @@ -2,7 +2,6 @@ package datastore import ( "fmt" - "os" "runtime/debug" portainer "github.com/portainer/portainer/api" @@ -16,6 +15,8 @@ import ( "github.com/rs/zerolog/log" ) +const beforePortainerVersionUpgradeBackup = "portainer.db.bak" + func (store *Store) MigrateData() error { updating, err := store.VersionService.IsUpdating() if err != nil { @@ -40,7 +41,7 @@ func (store *Store) MigrateData() error { } // before we alter anything in the DB, create a backup - _, err = store.Backup() + backupPath, err := store.Backup(version) if err != nil { return errors.Wrap(err, "while backing up database") } @@ -50,9 +51,9 @@ func (store *Store) MigrateData() error { err = errors.Wrap(err, "failed to migrate database") log.Warn().Err(err).Msg("migration failed, restoring database to previous version") - restoreErr := store.Restore() - if restoreErr != nil { - return errors.Wrap(restoreErr, "failed to restore database") + restorErr := store.restoreWithOptions(&BackupOptions{BackupPath: backupPath}) + if restorErr != nil { + return errors.Wrap(restorErr, "failed to restore database") } log.Info().Msg("database restored to previous version") @@ -116,11 +117,6 @@ 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") @@ -139,7 +135,9 @@ func (store *Store) connectionRollback(force bool) error { } } - err := store.Restore() + options := getBackupRestoreOptions(store.commonBackupDir()) + + err := store.restoreWithOptions(options) if err != nil { return err } diff --git a/api/datastore/migrate_data_test.go b/api/datastore/migrate_data_test.go index 5f731f5ce..7917bcd83 100644 --- a/api/datastore/migrate_data_test.go +++ b/api/datastore/migrate_data_test.go @@ -7,20 +7,29 @@ import ( "io" "os" "path/filepath" + "strings" "testing" - "github.com/Masterminds/semver" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/database/boltdb" - "github.com/portainer/portainer/api/database/models" - "github.com/portainer/portainer/api/datastore/migrator" - "github.com/rs/zerolog/log" "github.com/google/go-cmp/cmp" + "github.com/portainer/portainer/api/database/models" + "github.com/rs/zerolog/log" ) +// testVersion is a helper which tests current store version against wanted version +func testVersion(store *Store, versionWant string, t *testing.T) { + v, err := store.VersionService.Version() + if err != nil { + t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err) + } + if v.SchemaVersion != versionWant { + t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion) + } +} + func TestMigrateData(t *testing.T) { - tests := []struct { + snapshotTests := []struct { testName string srcPath string wantPath string @@ -33,7 +42,7 @@ func TestMigrateData(t *testing.T) { overrideInstanceId: true, }, } - for _, test := range tests { + for _, test := range snapshotTests { t.Run(test.testName, func(t *testing.T) { err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId) if err != nil { @@ -46,167 +55,147 @@ func TestMigrateData(t *testing.T) { }) } - 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") - } + // t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) { + // newStore, store, teardown := MustNewTestStore(t, true, false) + // defer teardown() - testVersion(store, portainer.APIVersion, t) - store.Close() + // if !newStore { + // t.Error("Expect a new DB") + // } - newStore, _ = store.Open() - if newStore { - t.Error("Expect store to NOT be new DB") - } - }) + // testVersion(store, portainer.APIVersion, t) + // store.Close() - t.Run("MigrateData should create backup file upon update", func(t *testing.T) { - _, store := MustNewTestStore(t, true, false) - store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "1.0", Edition: int(portainer.PortainerCE)}) - store.MigrateData() + // newStore, _ = store.Open() + // if newStore { + // t.Error("Expect store to NOT be new DB") + // } + // }) - backupfilename := store.backupFilename() - if exists, _ := store.fileService.FileExists(backupfilename); !exists { - t.Errorf("Expect backup file to be created %s", backupfilename) - } - }) + // 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() - t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) { - os.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL") + // // Setup data + // v := models.Version{SchemaVersion: tc.version} + // store.VersionService.UpdateVersion(&v) - version := "2.15" - _, store := MustNewTestStore(t, true, false) - store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)}) - store.MigrateData() + // // 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}) - store.Open() - testVersion(store, version, t) - }) + // t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) { + // store.MigrateData() + // testVersion(store, tc.expectedVersion, t) + // }) - t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) { - _, store := MustNewTestStore(t, true, false) - store.VersionService.StoreIsUpdating(true) - store.MigrateData() + // 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) + // }) + // } - // If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected! - // If the backup file is not blank, then it means a backup was created. We don't want that because we - // only create a backup when the version changes. - backupfilename := store.backupFilename() - if exists, _ := store.fileService.FileExists(backupfilename); exists { - t.Errorf("Backup file should not exist for dirty database") - } - }) + // t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) { + // _, store, teardown := MustNewTestStore(t, false, true) + // defer teardown() - t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) { - _, store := MustNewTestStore(t, true, false) + // v := models.Version{SchemaVersion: "1.24.1"} + // store.VersionService.UpdateVersion(&v) - // Set migrator the count to match our migrations array (simulate no changes). - // Should not create a backup - v, err := store.VersionService.Version() - if err != nil { - t.Errorf("Unable to read version from db: %s", err) - t.FailNow() - } + // store.MigrateData() - migratorParams := store.newMigratorParameters(v) - m := migrator.NewMigrator(migratorParams) - latestMigrations := m.LatestMigrations() + // testVersion(store, v.SchemaVersion, t) + // }) - if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) { - v.MigratorCount = len(latestMigrations.MigrationFuncs) - store.VersionService.UpdateVersion(v) - } + // t.Run("MigrateData should create backup file upon update", func(t *testing.T) { + // _, store, teardown := MustNewTestStore(t, false, true) + // defer teardown() - store.MigrateData() + // v := models.Version{SchemaVersion: "0.0.0"} + // store.VersionService.UpdateVersion(&v) - // If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected! - // If the backup file is not blank, then it means a backup was created. We don't want that because we - // only create a backup when the version changes. - backupfilename := store.backupFilename() - if exists, _ := store.fileService.FileExists(backupfilename); exists { - t.Errorf("Backup file should not exist for dirty database") - } - }) + // store.MigrateData() - t.Run("MigrateData should create backup on startup if portainer version matches db and migrationFuncs counts differ", func(t *testing.T) { - _, store := MustNewTestStore(t, true, false) + // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) - // Set migrator count very large to simulate changes - // Should not create a backup - v, err := store.VersionService.Version() - if err != nil { - t.Errorf("Unable to read version from db: %s", err) - t.FailNow() - } + // if !isFileExist(options.BackupPath) { + // t.Errorf("Backup file should exist; file=%s", options.BackupPath) + // } + // }) - v.MigratorCount = 1000 - store.VersionService.UpdateVersion(v) - store.MigrateData() + // 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() - // If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected! - // If the backup file is not blank, then it means a backup was created. We don't want that because we - // only create a backup when the version changes. - backupfilename := store.backupFilename() - if exists, _ := store.fileService.FileExists(backupfilename); !exists { - t.Errorf("DB backup should exist and there should be no error") - } - }) + // store.VersionService.StoreIsUpdating(true) + + // store.MigrateData() + + // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) + + // 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() + + // store.MigrateData() + + // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) + + // if isFileExist(options.BackupPath) { + // t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath) + // } + // }) +} + +func Test_getBackupRestoreOptions(t *testing.T) { + _, store := MustNewTestStore(t, false, true) + + options := getBackupRestoreOptions(store.commonBackupDir()) + + wantDir := store.commonBackupDir() + if !strings.HasSuffix(options.BackupDir, wantDir) { + log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir") + } + + wantFilename := "portainer.db.bak" + if options.BackupFileName != wantFilename { + log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file") + } } func TestRollback(t *testing.T) { t.Run("Rollback should restore upgrade after backup", func(t *testing.T) { - version := "2.11" - - v := models.Version{ - SchemaVersion: version, - } - - _, store := MustNewTestStore(t, false, false) - store.VersionService.UpdateVersion(&v) - - _, err := store.Backup() - if err != nil { - log.Fatal().Err(err).Msg("") - } - - v.SchemaVersion = "2.14" - // Change the current edition - err = store.VersionService.UpdateVersion(&v) - if err != nil { - log.Fatal().Err(err).Msg("") - } - - err = store.Rollback(true) - if err != nil { - t.Logf("Rollback failed: %s", err) - t.Fail() - return - } - - store.Open() - testVersion(store, version, t) - }) - - t.Run("Rollback should restore upgrade after backup", func(t *testing.T) { - version := "2.15" - - v := models.Version{ - SchemaVersion: version, - Edition: int(portainer.PortainerCE), - } - + version := models.Version{SchemaVersion: "2.4.0"} _, store := MustNewTestStore(t, true, false) - store.VersionService.UpdateVersion(&v) - _, err := store.Backup() + err := store.VersionService.UpdateVersion(&version) + if err != nil { + t.Errorf("Failed updating version: %v", err) + } + + _, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir())) if err != nil { log.Fatal().Err(err).Msg("") } - v.SchemaVersion = "2.14" - // Change the current edition - err = store.VersionService.UpdateVersion(&v) + // Change the current version + version2 := models.Version{SchemaVersion: "2.6.0"} + err = store.VersionService.UpdateVersion(&version2) if err != nil { log.Fatal().Err(err).Msg("") } @@ -218,11 +207,26 @@ func TestRollback(t *testing.T) { return } - store.Open() - testVersion(store, version, t) + _, err = store.Open() + if err != nil { + t.Logf("Open failed: %s", err) + t.Fail() + return + } + + testVersion(store, version.SchemaVersion, t) }) } +// isFileExist is helper function to check for file existence +func isFileExist(path string) bool { + matches, err := filepath.Glob(path) + if err != nil { + return false + } + return len(matches) > 0 +} + // migrateDBTestHelper loads a json representation of a bolt database from srcPath, // parses it into a database, runs a migration on that database, and then // compares it with an expected output database. diff --git a/api/portainer.go b/api/portainer.go index 9d871e701..2f502b178 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -2065,20 +2065,6 @@ const ( OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin" ) -// GetEditionLabel returns the portainer edition label -func (e SoftwareEdition) GetEditionLabel() string { - switch e { - case PortainerCE: - return "CE" - case PortainerBE: - return "BE" - case PortainerEE: - return "EE" - } - - return "CE" -} - const ( AzurePathContainerGroups = "/subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups" AzurePathContainerGroup = "/subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*"