mirror of https://github.com/portainer/portainer
pull/10843/head
parent
7408668dbb
commit
32e05bb705
|
@ -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(),
|
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(),
|
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(),
|
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(),
|
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
||||||
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").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(),
|
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||||
|
|
|
@ -4,81 +4,186 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (store *Store) Backup() (string, error) {
|
var backupDefaults = struct {
|
||||||
if err := store.createBackupPath(); err != nil {
|
backupDir string
|
||||||
return "", err
|
commonDir string
|
||||||
|
}{
|
||||||
|
"backups",
|
||||||
|
"common",
|
||||||
}
|
}
|
||||||
|
|
||||||
backupFilename := store.backupFilename()
|
//
|
||||||
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("Backing up database")
|
// Backup Helpers
|
||||||
|
//
|
||||||
|
|
||||||
// Close the store before backing up
|
// createBackupFolders create initial folders for backups
|
||||||
err := store.Close()
|
func (store *Store) createBackupFolders() {
|
||||||
if err != nil {
|
// create common dir
|
||||||
return "", fmt.Errorf("failed to close store before backup: %w", err)
|
commonDir := store.commonBackupDir()
|
||||||
}
|
if exists, _ := store.fileService.FileExists(commonDir); !exists {
|
||||||
|
if err := os.MkdirAll(commonDir, 0700); err != nil {
|
||||||
err = store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true)
|
log.Error().Err(err).Msg("error while creating common backup folder")
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create backup file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reopen the store
|
|
||||||
_, err = store.Open()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return backupFilename, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) Restore() error {
|
|
||||||
backupFilename := store.backupFilename()
|
|
||||||
return store.RestoreFromFile(backupFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) RestoreFromFile(backupFilename string) error {
|
|
||||||
if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil {
|
|
||||||
return fmt.Errorf("unable to restore backup file %q. err: %w", backupFilename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("database restored")
|
|
||||||
|
|
||||||
_, err := store.Open()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to determine version of restored portainer backup file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine the db version
|
|
||||||
version, err := store.VersionService.Version()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to determine restored database version. err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
editionLabel := portainer.SoftwareEdition(version.Edition).GetEditionLabel()
|
|
||||||
log.Info().Str("version", version.SchemaVersion).Msgf("Restored database version: Portainer %s %s ", editionLabel, 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 {
|
|
||||||
return fmt.Errorf("unable to create backup folder: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) backupFilename() string {
|
|
||||||
return path.Join(store.connection.GetStorePath(), "backups", store.connection.GetDatabaseFileName()+".bak")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) databasePath() string {
|
func (store *Store) databasePath() string {
|
||||||
return store.connection.GetDatabaseFilePath()
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,79 +2,106 @@ package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"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) {
|
func TestStoreCreation(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, true)
|
_, store := MustNewTestStore(t, true, true)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
t.Fatal("Expect to create a store")
|
t.Error("Expect to create a store")
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := store.VersionService.Version()
|
if store.CheckCurrentEdition() != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("")
|
|
||||||
}
|
|
||||||
|
|
||||||
if portainer.SoftwareEdition(v.Edition) != portainer.PortainerCE {
|
|
||||||
t.Error("Expect to get CE Edition")
|
t.Error("Expect to get CE Edition")
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.SchemaVersion != portainer.APIVersion {
|
|
||||||
t.Error("Expect to get APIVersion")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, true)
|
_, store := MustNewTestStore(t, true, true)
|
||||||
backupFileName := store.backupFilename()
|
connection := store.GetConnection()
|
||||||
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
|
|
||||||
|
t.Run("Backup should create default db backup", func(t *testing.T) {
|
||||||
v := models.Version{
|
v := models.Version{
|
||||||
Edition: int(portainer.PortainerCE),
|
|
||||||
SchemaVersion: portainer.APIVersion,
|
SchemaVersion: portainer.APIVersion,
|
||||||
}
|
}
|
||||||
store.VersionService.UpdateVersion(&v)
|
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) {
|
if !isFileExist(backupFileName) {
|
||||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRestore(t *testing.T) {
|
func TestRemoveWithOptions(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, false)
|
_, store := MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("Basic Restore"), func(t *testing.T) {
|
t.Run("successfully removes file if existent", func(t *testing.T) {
|
||||||
// override and set initial db version and edition
|
store.createBackupFolders()
|
||||||
updateEdition(store, portainer.PortainerCE)
|
options := &BackupOptions{
|
||||||
updateVersion(store, "2.4")
|
BackupDir: store.commonBackupDir(),
|
||||||
|
BackupFileName: "test.txt",
|
||||||
|
}
|
||||||
|
|
||||||
store.Backup()
|
filePath := path.Join(options.BackupDir, options.BackupFileName)
|
||||||
updateVersion(store, "2.16")
|
f, err := os.Create(filePath)
|
||||||
testVersion(store, "2.16", t)
|
if err != nil {
|
||||||
store.Restore()
|
t.Fatalf("file should be created; err=%s", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
// check if the restore is successful and the version is correct
|
err = store.removeWithOptions(options)
|
||||||
testVersion(store, "2.4", t)
|
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) {
|
t.Run("fails to removes file if non-existent", func(t *testing.T) {
|
||||||
// override and set initial db version and edition
|
options := &BackupOptions{
|
||||||
updateEdition(store, portainer.PortainerCE)
|
BackupDir: store.commonBackupDir(),
|
||||||
updateVersion(store, "2.4")
|
BackupFileName: "test.txt",
|
||||||
store.Backup()
|
}
|
||||||
updateVersion(store, "2.14")
|
|
||||||
updateVersion(store, "2.16")
|
|
||||||
testVersion(store, "2.16", t)
|
|
||||||
store.Restore()
|
|
||||||
|
|
||||||
// check if the restore is successful and the version is correct
|
err := store.removeWithOptions(options)
|
||||||
testVersion(store, "2.4", t)
|
if err == nil {
|
||||||
|
t.Error("RemoveWithOptions should fail for non-existent file")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,14 +31,8 @@ func (store *Store) Open() (newStore bool, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if encryptionReq {
|
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()
|
err = store.encryptDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
store.RestoreFromFile(backupFilename) // restore from backup if encryption fails
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -16,6 +15,8 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||||
|
|
||||||
func (store *Store) MigrateData() error {
|
func (store *Store) MigrateData() error {
|
||||||
updating, err := store.VersionService.IsUpdating()
|
updating, err := store.VersionService.IsUpdating()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,7 +41,7 @@ func (store *Store) MigrateData() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// before we alter anything in the DB, create a backup
|
// before we alter anything in the DB, create a backup
|
||||||
_, err = store.Backup()
|
backupPath, err := store.Backup(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "while backing up database")
|
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")
|
err = errors.Wrap(err, "failed to migrate database")
|
||||||
|
|
||||||
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
||||||
restoreErr := store.Restore()
|
restorErr := store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||||
if restoreErr != nil {
|
if restorErr != nil {
|
||||||
return errors.Wrap(restoreErr, "failed to restore database")
|
return errors.Wrap(restorErr, "failed to restore database")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("database restored to previous version")
|
log.Info().Msg("database restored to previous version")
|
||||||
|
@ -116,11 +117,6 @@ func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models
|
||||||
return err
|
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)
|
err = store.VersionService.StoreIsUpdating(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to update the store")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,20 +7,29 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/database/boltdb"
|
"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/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) {
|
func TestMigrateData(t *testing.T) {
|
||||||
tests := []struct {
|
snapshotTests := []struct {
|
||||||
testName string
|
testName string
|
||||||
srcPath string
|
srcPath string
|
||||||
wantPath string
|
wantPath string
|
||||||
|
@ -33,7 +42,7 @@ func TestMigrateData(t *testing.T) {
|
||||||
overrideInstanceId: true,
|
overrideInstanceId: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range snapshotTests {
|
||||||
t.Run(test.testName, func(t *testing.T) {
|
t.Run(test.testName, func(t *testing.T) {
|
||||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
||||||
if err != nil {
|
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) {
|
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||||
newStore, store := MustNewTestStore(t, true, false)
|
// newStore, store, teardown := MustNewTestStore(t, true, false)
|
||||||
if !newStore {
|
// defer teardown()
|
||||||
t.Error("Expect a new DB")
|
|
||||||
|
// if !newStore {
|
||||||
|
// t.Error("Expect a new DB")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// testVersion(store, portainer.APIVersion, t)
|
||||||
|
// store.Close()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// // Setup data
|
||||||
|
// v := models.Version{SchemaVersion: tc.version}
|
||||||
|
// 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})
|
||||||
|
|
||||||
|
// 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("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||||
|
// _, store, teardown := MustNewTestStore(t, false, true)
|
||||||
|
// defer teardown()
|
||||||
|
|
||||||
|
// v := models.Version{SchemaVersion: "1.24.1"}
|
||||||
|
// store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
|
// store.MigrateData()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// v := models.Version{SchemaVersion: "0.0.0"}
|
||||||
|
// store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
|
// store.MigrateData()
|
||||||
|
|
||||||
|
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
testVersion(store, portainer.APIVersion, t)
|
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||||
store.Close()
|
_, store := MustNewTestStore(t, false, true)
|
||||||
|
|
||||||
newStore, _ = store.Open()
|
options := getBackupRestoreOptions(store.commonBackupDir())
|
||||||
if newStore {
|
|
||||||
t.Error("Expect store to NOT be new DB")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
wantDir := store.commonBackupDir()
|
||||||
_, store := MustNewTestStore(t, true, false)
|
if !strings.HasSuffix(options.BackupDir, wantDir) {
|
||||||
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "1.0", Edition: int(portainer.PortainerCE)})
|
log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
|
||||||
store.MigrateData()
|
|
||||||
|
|
||||||
backupfilename := store.backupFilename()
|
|
||||||
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
|
|
||||||
t.Errorf("Expect backup file to be created %s", backupfilename)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) {
|
|
||||||
os.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL")
|
|
||||||
|
|
||||||
version := "2.15"
|
|
||||||
_, store := MustNewTestStore(t, true, false)
|
|
||||||
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)})
|
|
||||||
store.MigrateData()
|
|
||||||
|
|
||||||
store.Open()
|
|
||||||
testVersion(store, version, 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()
|
|
||||||
|
|
||||||
// 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("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
|
||||||
_, store := MustNewTestStore(t, true, false)
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
migratorParams := store.newMigratorParameters(v)
|
wantFilename := "portainer.db.bak"
|
||||||
m := migrator.NewMigrator(migratorParams)
|
if options.BackupFileName != wantFilename {
|
||||||
latestMigrations := m.LatestMigrations()
|
log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
|
||||||
|
|
||||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
|
||||||
v.MigratorCount = len(latestMigrations.MigrationFuncs)
|
|
||||||
store.VersionService.UpdateVersion(v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
store.MigrateData()
|
|
||||||
|
|
||||||
// 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("MigrateData should create backup on startup if portainer version matches db and migrationFuncs counts differ", func(t *testing.T) {
|
|
||||||
_, store := MustNewTestStore(t, true, false)
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
v.MigratorCount = 1000
|
|
||||||
store.VersionService.UpdateVersion(v)
|
|
||||||
store.MigrateData()
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRollback(t *testing.T) {
|
func TestRollback(t *testing.T) {
|
||||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||||
version := "2.11"
|
version := models.Version{SchemaVersion: "2.4.0"}
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, store := MustNewTestStore(t, true, false)
|
_, 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 {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
|
|
||||||
v.SchemaVersion = "2.14"
|
// Change the current version
|
||||||
// Change the current edition
|
version2 := models.Version{SchemaVersion: "2.6.0"}
|
||||||
err = store.VersionService.UpdateVersion(&v)
|
err = store.VersionService.UpdateVersion(&version2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
|
@ -218,11 +207,26 @@ func TestRollback(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.Open()
|
_, err = store.Open()
|
||||||
testVersion(store, version, t)
|
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,
|
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
||||||
// parses it into a database, runs a migration on that database, and then
|
// parses it into a database, runs a migration on that database, and then
|
||||||
// compares it with an expected output database.
|
// compares it with an expected output database.
|
||||||
|
|
|
@ -2065,20 +2065,6 @@ const (
|
||||||
OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin"
|
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 (
|
const (
|
||||||
AzurePathContainerGroups = "/subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups"
|
AzurePathContainerGroups = "/subscriptions/*/providers/Microsoft.ContainerInstance/containerGroups"
|
||||||
AzurePathContainerGroup = "/subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*"
|
AzurePathContainerGroup = "/subscriptions/*/resourceGroups/*/providers/Microsoft.ContainerInstance/containerGroups/*"
|
||||||
|
|
Loading…
Reference in New Issue