feat(version): migrate version to semver [EE-3756] (#7693)

redisigned version bucket and migration code
pull/8080/head
Matt Hook 2022-11-18 13:18:09 +13:00 committed by GitHub
parent 4cfa584c7c
commit 583346321e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 747 additions and 509 deletions

View File

@ -2,14 +2,14 @@ package cli
import ( import (
"bufio" "bufio"
"log" "fmt"
"os" "os"
"strings" "strings"
) )
// Confirm starts a rollback db cli application // Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) { func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message) fmt.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n') answer, err := reader.ReadString('\n')

View File

@ -19,6 +19,7 @@ import (
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database" "github.com/portainer/portainer/api/database"
"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/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/demo" "github.com/portainer/portainer/api/demo"
@ -43,6 +44,7 @@ import (
"github.com/portainer/portainer/api/scheduler" "github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments" "github.com/portainer/portainer/api/stacks/deployments"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -98,8 +100,6 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Info().Msg("exiting rollback") log.Info().Msg("exiting rollback")
os.Exit(0) os.Exit(0)
return nil
} }
// Init sets some defaults - it's basically a migration // Init sets some defaults - it's basically a migration
@ -109,26 +109,29 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
} }
if isNew { if isNew {
// from MigrateData instanceId, err := uuid.NewV4()
store.VersionService.StoreDBVersion(portainer.DBVersion) if err != nil {
log.Fatal().Err(err).Msg("failed generating instance id")
}
err := updateSettingsFromFlags(store, flags) // from MigrateData
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
InstanceID: instanceId.String(),
}
store.VersionService.UpdateVersion(&v)
err = updateSettingsFromFlags(store, flags)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags") log.Fatal().Err(err).Msg("failed updating settings from flags")
} }
} else { } else {
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
log.Fatal().Err(err).Msg("failure during creation of new database")
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData() err = store.MigrateData()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed migration") log.Fatal().Err(err).Msg("failed migration")
} }
} }
}
err = updateSettingsFromFlags(store, flags) err = updateSettingsFromFlags(store, flags)
if err != nil { if err != nil {

View File

@ -0,0 +1,8 @@
package models
type Version struct {
SchemaVersion string
MigratorCount int
Edition int
InstanceID string
}

View File

@ -7,4 +7,5 @@ var (
ErrObjectNotFound = errors.New("object not found inside the database") ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/") ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed") ErrDBImportFailed = errors.New("importing backup failed")
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
) )

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"time" "time"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/errors" "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/edgetypes" "github.com/portainer/portainer/api/edgetypes"
@ -303,12 +304,11 @@ type (
// VersionService represents a service for managing version data // VersionService represents a service for managing version data
VersionService interface { VersionService interface {
DBVersion() (int, error)
Edition() (portainer.SoftwareEdition, error) Edition() (portainer.SoftwareEdition, error)
InstanceID() (string, error) InstanceID() (string, error)
StoreDBVersion(version int) error UpdateInstanceID(ID string) error
StoreInstanceID(ID string) error Version() (*models.Version, error)
BucketName() string UpdateVersion(*models.Version) error
} }
// WebhookService represents a service for managing webhook data. // WebhookService represents a service for managing webhook data.

View File

@ -1,17 +1,18 @@
package version package version
import ( import (
"strconv" "errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
) )
const ( const (
// BucketName represents the name of the bucket where this service stores data. // BucketName represents the name of the bucket where this service stores data.
BucketName = "version" BucketName = "version"
versionKey = "DB_VERSION" versionKey = "VERSION"
instanceKey = "INSTANCE_ID"
editionKey = "EDITION"
updatingKey = "DB_UPDATING" updatingKey = "DB_UPDATING"
) )
@ -20,10 +21,6 @@ type Service struct {
connection portainer.Connection connection portainer.Connection
} }
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) { func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName) err := connection.SetServiceName(BucketName)
@ -36,56 +33,87 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil }, nil
} }
// DBVersion retrieves the stored database version. func (service *Service) SchemaVersion() (string, error) {
func (service *Service) DBVersion() (int, error) { v, err := service.Version()
var version string
err := service.connection.GetObject(BucketName, []byte(versionKey), &version)
if err != nil { if err != nil {
return 0, err return "", err
} }
return strconv.Atoi(version)
return v.SchemaVersion, nil
}
func (service *Service) UpdateSchemaVersion(version string) error {
v, err := service.Version()
if err != nil {
return err
}
v.SchemaVersion = version
return service.UpdateVersion(v)
} }
// Edition retrieves the stored portainer edition.
func (service *Service) Edition() (portainer.SoftwareEdition, error) { func (service *Service) Edition() (portainer.SoftwareEdition, error) {
var edition string v, err := service.Version()
err := service.connection.GetObject(BucketName, []byte(editionKey), &edition)
if err != nil { if err != nil {
return 0, err return 0, err
} }
e, err := strconv.Atoi(edition)
if err != nil {
return 0, err
}
return portainer.SoftwareEdition(e), nil
}
// StoreDBVersion store the database version. return portainer.SoftwareEdition(v.Edition), nil
func (service *Service) StoreDBVersion(version int) error {
return service.connection.UpdateObject(BucketName, []byte(versionKey), strconv.Itoa(version))
} }
// IsUpdating retrieves the database updating status. // IsUpdating retrieves the database updating status.
func (service *Service) IsUpdating() (bool, error) { func (service *Service) IsUpdating() (bool, error) {
var isUpdating bool var isUpdating bool
err := service.connection.GetObject(BucketName, []byte(updatingKey), &isUpdating) err := service.connection.GetObject(BucketName, []byte(updatingKey), &isUpdating)
if err != nil && errors.Is(err, dserrors.ErrObjectNotFound) {
return false, nil
}
return isUpdating, err return isUpdating, err
} }
// StoreIsUpdating store the database updating status. // StoreIsUpdating store the database updating status.
func (service *Service) StoreIsUpdating(isUpdating bool) error { func (service *Service) StoreIsUpdating(isUpdating bool) error {
return service.connection.UpdateObject(BucketName, []byte(updatingKey), isUpdating) return service.connection.DeleteObject(BucketName, []byte(updatingKey))
} }
// InstanceID retrieves the stored instance ID. // InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) { func (service *Service) InstanceID() (string, error) {
var id string v, err := service.Version()
err := service.connection.GetObject(BucketName, []byte(instanceKey), &id) if err != nil {
return id, err return "", err
}
return v.InstanceID, nil
} }
// StoreInstanceID store the instance ID. // StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error { func (service *Service) UpdateInstanceID(id string) error {
return service.connection.UpdateObject(BucketName, []byte(instanceKey), ID) v, err := service.Version()
if err != nil {
if !dataservices.IsErrObjectNotFound(err) {
return err
}
v = &models.Version{}
}
v.InstanceID = id
return service.UpdateVersion(v)
}
// Version retrieve the version object.
func (service *Service) Version() (*models.Version, error) {
var v models.Version
err := service.connection.GetObject(BucketName, []byte(versionKey), &v)
if err != nil {
return nil, err
}
return &v, nil
}
// UpdateVersion persists a Version object.
func (service *Service) UpdateVersion(version *models.Version) error {
return service.connection.UpdateObject(BucketName, []byte(versionKey), version)
} }

View File

@ -6,6 +6,7 @@ import (
"path" "path"
"time" "time"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -53,7 +54,7 @@ func (store *Store) copyDBFile(from string, to string) error {
// BackupOptions provide a helper to inject backup options // BackupOptions provide a helper to inject backup options
type BackupOptions struct { type BackupOptions struct {
Version int // I can't find this used for anything other than a filename Version string
BackupDir string BackupDir string
BackupFileName string BackupFileName string
BackupPath string BackupPath string
@ -70,26 +71,32 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
} }
// Backup current database with default options // Backup current database with default options
func (store *Store) Backup() (string, error) { func (store *Store) Backup(version *models.Version) (string, error) {
if version == nil {
return store.backupWithOptions(nil) return store.backupWithOptions(nil)
} }
return store.backupWithOptions(&BackupOptions{
Version: version.SchemaVersion,
})
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions { func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil { if options == nil {
options = &BackupOptions{} options = &BackupOptions{}
} }
if options.Version == 0 { if options.Version == "" {
version, err := store.version() v, err := store.VersionService.Version()
if err != nil { if err != nil {
version = 0 options.Version = ""
} }
options.Version = version options.Version = v.SchemaVersion
} }
if options.BackupDir == "" { if options.BackupDir == "" {
options.BackupDir = store.commonBackupDir() options.BackupDir = store.commonBackupDir()
} }
if options.BackupFileName == "" { if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405")) options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), options.Version, time.Now().Format("20060102150405"))
} }
if options.BackupPath == "" { if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName) options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
@ -168,7 +175,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist") log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
return err return err
} }
@ -176,7 +182,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
err = os.Remove(options.BackupPath) err = os.Remove(options.BackupPath)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed") log.Error().Err(err).Msg("failed")
return err return err
} }

View File

@ -7,10 +7,11 @@ import (
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
) )
func TestCreateBackupFolders(t *testing.T) { func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true) _, store, teardown := MustNewTestStore(t, true, true)
defer teardown() defer teardown()
connection := store.GetConnection() connection := store.GetConnection()
@ -45,10 +46,13 @@ func TestBackup(t *testing.T) {
defer teardown() defer teardown()
t.Run("Backup should create default db backup", func(t *testing.T) { t.Run("Backup should create default db backup", func(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion) v := models.Version{
SchemaVersion: portainer.APIVersion,
}
store.VersionService.UpdateVersion(&v)
store.backupWithOptions(nil) store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion)) backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion))
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)
} }

View File

@ -13,22 +13,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if store.IsErrObjectNotFound(err) {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// NewStore initializes a new Store and the associated services // NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store { func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
return &Store{ return &Store{
@ -39,8 +23,6 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
// Open opens and initializes the BoltDB database. // Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) { func (store *Store) Open() (newStore bool, err error) {
newStore = true
encryptionReq, err := store.connection.NeedsEncryptionMigration() encryptionReq, err := store.connection.NeedsEncryptionMigration()
if err != nil { if err != nil {
return false, err return false, err
@ -55,33 +37,26 @@ func (store *Store) Open() (newStore bool, err error) {
err = store.connection.Open() err = store.connection.Open()
if err != nil { if err != nil {
return newStore, err return false, err
} }
err = store.initServices() err = store.initServices()
if err != nil { if err != nil {
return newStore, err return false, err
} }
// if we have DBVersion in the database then ensure we flag this as NOT a new store // If no settings object exists then assume we have a new store
version, err := store.VersionService.DBVersion() _, err = store.SettingsService.Settings()
if err != nil { if err != nil {
if store.IsErrObjectNotFound(err) { if store.IsErrObjectNotFound(err) {
return newStore, nil return true, nil
} }
return false, err
return newStore, err
} }
if version > 0 {
log.Debug().Int("version", version).Msg("opened existing store")
return false, nil return false, nil
} }
return newStore, nil
}
func (store *Store) Close() error { func (store *Store) Close() error {
return store.connection.Close() return store.connection.Close()
} }
@ -94,17 +69,29 @@ func (store *Store) BackupTo(w io.Writer) error {
// CheckCurrentEdition checks if current edition is community edition // CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error { func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE { if store.edition() != portainer.Edition {
return portainerErrors.ErrWrongDBEdition return portainerErrors.ErrWrongDBEdition
} }
return nil return nil
} }
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()? // TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool { func (store *Store) IsErrObjectNotFound(e error) bool {
return e == portainerErrors.ErrObjectNotFound return e == portainerErrors.ErrObjectNotFound
} }
func (store *Store) Connection() portainer.Connection {
return store.connection
}
func (store *Store) Rollback(force bool) error { func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force) return store.connectionRollback(force)
} }

View File

@ -1,18 +1,12 @@
package datastore package datastore
import ( import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
// Init creates the default data set. // Init creates the default data set.
func (store *Store) Init() error { func (store *Store) Init() error {
err := store.checkOrCreateInstanceID() err := store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSettings()
if err != nil { if err != nil {
return err return err
} }
@ -25,21 +19,6 @@ func (store *Store) Init() error {
return store.checkOrCreateDefaultData() return store.checkOrCreateDefaultData()
} }
func (store *Store) checkOrCreateInstanceID() error {
_, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID := uid.String()
return store.VersionService.StoreInstanceID(instanceID)
}
return err
}
func (store *Store) checkOrCreateDefaultSettings() error { func (store *Store) checkOrCreateDefaultSettings() error {
// TODO: these need to also be applied when importing // TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings() settings, err := store.SettingsService.Settings()

View File

@ -4,37 +4,69 @@ import (
"fmt" "fmt"
"runtime/debug" "runtime/debug"
portainer "github.com/portainer/portainer/api" portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/dataservices/errors" "github.com/portainer/portainer/api/database/models"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore/migrator" "github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
werrors "github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const beforePortainerVersionUpgradeBackup = "portainer.db.bak" const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
func (store *Store) MigrateData() error { func (store *Store) MigrateData() error {
version, err := store.version() updating, err := store.VersionService.IsUpdating()
if err != nil { if err != nil {
return err return errors.Wrap(err, "while checking if the store is updating")
} }
// Backup Database if updating {
backupPath, err := store.Backup() return dserrors.ErrDatabaseIsUpdating
if err != nil {
return werrors.Wrap(err, "while backing up db before migration")
} }
migratorParams := &migrator.MigratorParameters{ // migrate new version bucket if required (doesn't write anything to db yet)
DatabaseVersion: version, version, err := store.getOrMigrateLegacyVersion()
if err != nil {
return errors.Wrap(err, "while migrating legacy version")
}
migratorParams := store.newMigratorParameters(version)
migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() {
return nil
}
// before we alter anything in the DB, create a backup
backupPath, err := store.Backup(version)
if err != nil {
return errors.Wrap(err, "while backing up database")
}
err = store.FailSafeMigrate(migrator, version)
if err != nil {
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")
}
log.Info().Msg("database restored to previous version")
return errors.Wrap(err, "failed to migrate database")
}
return nil
}
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
return &migrator.MigratorParameters{
CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService, EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService, ExtensionService: store.ExtensionService,
FDOProfilesService: store.FDOProfilesService,
RegistryService: store.RegistryService, RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService, ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService, RoleService: store.RoleService,
@ -50,96 +82,38 @@ func (store *Store) MigrateData() error {
DockerhubService: store.DockerHubService, DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store), AuthorizationService: authorization.NewService(store),
} }
// restore on error
err = store.connectionMigrateData(migratorParams)
if err != nil {
log.Error().Err(err).Msg("while DB migration, restoring DB")
// Restore options
options := BackupOptions{
BackupPath: backupPath,
}
err := store.restoreWithOptions(&options)
if err != nil {
log.Fatal().
Str("database_file", store.databasePath()).
Str("backup", options.BackupPath).Err(err).
Msg("failed restoring the backup, Portainer database file needs to restored manually by replacing the database file with a recent backup")
}
}
return err
} }
// FailSafeMigrate backup and restore DB if migration fail // FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) { func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
store.Rollback(true)
// return error with cause and stacktrace (recover() doesn't include a stacktrace) // return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack())) err = fmt.Errorf("%v %s", e, string(debug.Stack()))
} }
}() }()
// !Important: we must use a named return value in the function definition and not a local err = store.VersionService.StoreIsUpdating(true)
// !variable referenced from the closure or else the return value will be incorrectly set if err != nil {
return migrator.Migrate() return errors.Wrap(err, "while updating the store")
} }
// MigrateData automatically migrate the data based on the DBVersion. // now update the version to the new struct (if required)
// This process is only triggered on an existing database, not if the database was just created. err = store.finishMigrateLegacyVersion(version)
// if force is true, then migrate regardless. if err != nil {
func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParameters) error { return errors.Wrap(err, "while updating version")
migrator := migrator.NewMigrator(migratorParams) }
// backup db file before upgrading DB to support rollback log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portaineree.APIVersion)
isUpdating, err := migratorParams.VersionService.IsUpdating()
if err != nil && err != errors.ErrObjectNotFound { err = migrator.Migrate()
if err != nil {
return err return err
} }
if !isUpdating && migrator.Version() != portainer.DBVersion { err = store.VersionService.StoreIsUpdating(false)
err = store.backupVersion(migrator)
if err != nil { if err != nil {
return werrors.Wrapf(err, "failed to backup database") return errors.Wrap(err, "failed to update the store")
}
}
if migrator.Version() < portainer.DBVersion {
log.Info().
Int("migrator_version", migrator.Version()).
Int("db_version", portainer.DBVersion).
Msg("migrating database")
err = store.FailSafeMigrate(migrator)
if err != nil {
log.Error().Err(err).Msg("an error occurred during database migration")
return err
}
}
return nil
}
// backupVersion will backup the database or panic if any errors occur
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
log.Info().Msg("backing up database prior to version upgrade")
options := getBackupRestoreOptions(store.commonBackupDir())
_, err := store.backupWithOptions(options)
if err != nil {
log.Error().Err(err).Msg("an error occurred during database backup")
removalErr := store.removeWithOptions(options)
if removalErr != nil {
log.Error().Err(err).Msg("an error occurred during store removal prior to backup")
}
return err
} }
return nil return nil

View File

@ -10,21 +10,21 @@ import (
"strings" "strings"
"testing" "testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb" "github.com/portainer/portainer/api/database/boltdb"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// testVersion is a helper which tests current store version against wanted version // testVersion is a helper which tests current store version against wanted version
func testVersion(store *Store, versionWant int, t *testing.T) { func testVersion(store *Store, versionWant string, t *testing.T) {
v, err := store.VersionService.DBVersion() v, err := store.VersionService.Version()
if err != nil { if err != nil {
t.Errorf("Expect store version to be %d but was %d with error: %s", versionWant, v, err) t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err)
} }
if v != versionWant { if v.SchemaVersion != versionWant {
t.Errorf("Expect store version to be %d but was %d", versionWant, v) t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
} }
} }
@ -33,16 +33,18 @@ func TestMigrateData(t *testing.T) {
testName string testName string
srcPath string srcPath string
wantPath string wantPath string
overrideInstanceId bool
}{ }{
{ {
testName: "migrate version 24 to latest", testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json", srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json", wantPath: "test_data/output_24_to_latest.json",
overrideInstanceId: true,
}, },
} }
for _, test := range snapshotTests { 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) err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
if err != nil { if err != nil {
t.Errorf( t.Errorf(
"Failed migrating mock database %v: %v", "Failed migrating mock database %v: %v",
@ -53,111 +55,111 @@ 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, teardown := MustNewTestStore(t, false, true) // newStore, store, teardown := MustNewTestStore(t, true, false)
defer teardown() // defer teardown()
if !newStore { // if !newStore {
t.Error("Expect a new DB") // 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)
// not called for new stores
// store.MigrateData() // store.MigrateData()
testVersion(store, portainer.DBVersion, t) // testVersion(store, v.SchemaVersion, t)
store.Close() // })
newStore, _ = store.Open() // t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
if newStore { // _, store, teardown := MustNewTestStore(t, false, true)
t.Error("Expect store to NOT be new DB") // defer teardown()
}
})
tests := []struct { // v := models.Version{SchemaVersion: "0.0.0"}
version int // store.VersionService.UpdateVersion(&v)
expectedVersion int
}{
{version: 17, expectedVersion: portainer.DBVersion},
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
// Setup data // store.MigrateData()
store.VersionService.StoreDBVersion(tc.version)
// Required roles by migrations 22.2 // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
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 %d", tc.version), func(t *testing.T) { // if !isFileExist(options.BackupPath) {
store.MigrateData() // t.Errorf("Backup file should exist; file=%s", options.BackupPath)
testVersion(store, tc.expectedVersion, t) // }
}) // })
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) { // t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
store.Rollback(true) // _, store, teardown := MustNewTestStore(t, false, true)
store.Open() // defer teardown()
testVersion(store, tc.version, t)
})
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) { // store.VersionService.StoreIsUpdating(true)
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
version := 17 // store.MigrateData()
store.VersionService.StoreDBVersion(version)
store.MigrateData() // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
testVersion(store, version, t) // if isFileExist(options.BackupPath) {
}) // t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
// }
// })
t.Run("MigrateData should create backup file upon update", func(t *testing.T) { // t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true) // _, store, teardown := MustNewTestStore(t, false, true)
defer teardown() // defer teardown()
store.VersionService.StoreDBVersion(0)
store.MigrateData() // store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir())) // options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if !isFileExist(options.BackupPath) { // if isFileExist(options.BackupPath) {
t.Errorf("Backup file should exist; file=%s", options.BackupPath) // t.Errorf("Backup file should not exist for dirty database; 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)
}
})
} }
func Test_getBackupRestoreOptions(t *testing.T) { func Test_getBackupRestoreOptions(t *testing.T) {
@ -179,18 +181,23 @@ func Test_getBackupRestoreOptions(t *testing.T) {
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 := 21 version := models.Version{SchemaVersion: "2.4.0"}
_, store, teardown := MustNewTestStore(t, false, true) _, store, teardown := MustNewTestStore(t, true, false)
defer teardown() defer teardown()
store.VersionService.StoreDBVersion(version)
_, err := store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir())) 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("")
} }
// Change the current edition // Change the current version
err = store.VersionService.StoreDBVersion(version + 10) version2 := models.Version{SchemaVersion: "2.6.0"}
err = store.VersionService.UpdateVersion(&version2)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@ -199,12 +206,17 @@ func TestRollback(t *testing.T) {
if err != nil { if err != nil {
t.Logf("Rollback failed: %s", err) t.Logf("Rollback failed: %s", err)
t.Fail() t.Fail()
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)
}) })
} }
@ -220,15 +232,20 @@ func isFileExist(path string) bool {
// 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.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error { func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
srcJSON, err := os.ReadFile(srcPath) srcJSON, err := os.ReadFile(srcPath)
if err != nil { if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err) t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
} }
// Parse source json to db. // Parse source json to db.
_, store, teardown := MustNewTestStore(t, true, false) // When we create a new test store, it sets its version field automatically to latest.
defer teardown() _, store, _ := MustNewTestStore(t, true, false)
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
store.connection.DeleteObject("version", []byte("VERSION"))
// defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store) err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil { if err != nil {
return err return err
@ -240,6 +257,21 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
return err return err
} }
if overrideInstanceId {
// old versions of portainer did not have instance-id. Because this gets generated
// we need to override the expected output to match the expected value to pass the test
v, err := store.VersionService.Version()
if err != nil {
return err
}
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
err = store.VersionService.UpdateVersion(v)
if err != nil {
return err
}
}
// Assert that our database connection is using bolt so we can call // Assert that our database connection is using bolt so we can call
// exportJson rather than ExportRaw. The exportJson function allows us to // exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests. // strip out the metadata which we don't want for our tests.
@ -316,11 +348,23 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
t.Logf("failed casting %s to map[string]interface{}", k) t.Logf("failed casting %s to map[string]interface{}", k)
} }
dbVersion, ok := versions["DB_VERSION"] // New format db
if !ok { version, ok := versions["VERSION"]
t.Logf("failed getting DB_VERSION from %s", k) if ok {
err := con.CreateObjectWithStringId(
k,
[]byte("VERSION"),
version,
)
if err != nil {
t.Logf("failed writing VERSION in %s: %v", k, err)
}
} }
// old format db
dbVersion, ok := versions["DB_VERSION"]
if ok {
numDBVersion, ok := dbVersion.(json.Number) numDBVersion, ok := dbVersion.(json.Number)
if !ok { if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k) t.Logf("failed parsing DB_VERSION as json number from %s", k)
@ -339,12 +383,10 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
if err != nil { if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err) t.Logf("failed writing DB_VERSION in %s: %v", k, err)
} }
instanceID, ok := versions["INSTANCE_ID"]
if !ok {
t.Logf("failed getting INSTANCE_ID from %s", k)
} }
instanceID, ok := versions["INSTANCE_ID"]
if ok {
err = con.CreateObjectWithStringId( err = con.CreateObjectWithStringId(
k, k,
[]byte("INSTANCE_ID"), []byte("INSTANCE_ID"),
@ -353,6 +395,19 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
if err != nil { if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err) t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
} }
}
edition, ok := versions["EDITION"]
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("EDITION"),
edition,
)
if err != nil {
t.Logf("failed writing EDITION in %s: %v", k, err)
}
}
case "dockerhub": case "dockerhub":
obj, ok := v.([]interface{}) obj, ok := v.([]interface{})

View File

@ -54,7 +54,6 @@ func TestMigrateSettings(t *testing.T) {
} }
m := migrator.NewMigrator(&migrator.MigratorParameters{ m := migrator.NewMigrator(&migrator.MigratorParameters{
DatabaseVersion: 29,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService, EndpointRelationService: store.EndpointRelationService,

View File

@ -0,0 +1,114 @@
package datastore
import (
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
)
const (
bucketName = "version"
legacyDBVersionKey = "DB_VERSION"
legacyInstanceKey = "INSTANCE_ID"
legacyEditionKey = "EDITION"
)
var dbVerToSemVerMap = map[int]string{
18: "1.21",
19: "1.22",
20: "1.22.1",
21: "1.22.2",
22: "1.23",
23: "1.24",
24: "1.24.1",
25: "2.0",
26: "2.1",
27: "2.2",
28: "2.4",
29: "2.4",
30: "2.6",
31: "2.7",
32: "2.9",
33: "2.9.1",
34: "2.10",
35: "2.9.3",
36: "2.11",
40: "2.13",
50: "2.14",
51: "2.14.1",
52: "2.14.2",
60: "2.15",
61: "2.15.1",
70: "2.16",
}
func dbVersionToSemanticVersion(dbVersion int) string {
if dbVersion < 18 {
return "1.0.0"
}
ver, ok := dbVerToSemVerMap[dbVersion]
if ok {
return ver
}
// We should always return something sensible
switch {
case dbVersion < 40:
return "2.11"
case dbVersion < 50:
return "2.13"
case dbVersion < 60:
return "2.14.2"
case dbVersion < 70:
return "2.15.1"
}
return "2.16.0"
}
// getOrMigrateLegacyVersion to new Version struct
func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
// Very old versions of portainer did not have a version bucket, lets set some defaults
dbVersion := 24
edition := int(portaineree.PortainerCE)
instanceId := ""
// If we already have a version key, we don't need to migrate
v, err := store.VersionService.Version()
if err == nil || !dataservices.IsErrObjectNotFound(err) {
return v, err
}
err = store.connection.GetObject(bucketName, []byte(legacyDBVersionKey), &dbVersion)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
err = store.connection.GetObject(bucketName, []byte(legacyEditionKey), &edition)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
err = store.connection.GetObject(bucketName, []byte(legacyInstanceKey), &instanceId)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
return &models.Version{
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
Edition: edition,
InstanceID: string(instanceId),
}, nil
}
// finishMigrateLegacyVersion writes the new version to the DB and removes the old version keys from the DB
func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) error {
err := store.VersionService.UpdateVersion(versionToWrite)
// Remove legacy keys if present
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
return err
}

View File

@ -4,143 +4,124 @@ import (
"reflect" "reflect"
"runtime" "runtime"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/pkg/errors" "github.com/Masterminds/semver"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type migration struct {
dbversion int
migrate func() error
}
func migrationError(err error, context string) error { func migrationError(err error, context string) error {
return errors.Wrap(err, "failed in "+context) return errors.Wrap(err, "failed in "+context)
} }
func newMigration(dbversion int, migrate func() error) migration {
return migration{
dbversion: dbversion,
migrate: migrate,
}
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
func GetFunctionName(i interface{}) string { func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
} }
// Migrate checks the database version and migrate the existing data to the most recent data model. // Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error { func (m *Migrator) Migrate() error {
// set DB to updating status version, err := m.versionService.Version()
err := m.versionService.StoreIsUpdating(true)
if err != nil { if err != nil {
return migrationError(err, "StoreIsUpdating") return migrationError(err, "get version service")
} }
migrations := []migration{ schemaVersion, err := semver.NewVersion(version.SchemaVersion)
// Portainer < 1.21.0
newMigration(17, dbTooOldError),
// Portainer 1.21.0
newMigration(18, m.updateUsersToDBVersion18),
newMigration(18, m.updateEndpointsToDBVersion18),
newMigration(18, m.updateEndpointGroupsToDBVersion18),
newMigration(18, m.updateRegistriesToDBVersion18),
// 1.22.0
newMigration(19, m.updateSettingsToDBVersion19),
// 1.22.1
newMigration(20, m.updateUsersToDBVersion20),
newMigration(20, m.updateSettingsToDBVersion20),
newMigration(20, m.updateSchedulesToDBVersion20),
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
newMigration(22, m.updateResourceControlsToDBVersion22),
newMigration(22, m.updateUsersAndRolesToDBVersion22),
// Portainer 1.24.0
newMigration(23, m.updateTagsToDBVersion23),
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
// Portainer 1.24.1
newMigration(24, m.updateSettingsToDB24),
// Portainer 2.0.0
newMigration(25, m.updateSettingsToDB25),
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
// Portainer 2.1.0
newMigration(26, m.updateEndpointSettingsToDB25),
// Portainer 2.2.0
newMigration(27, m.updateStackResourceControlToDB27),
// Portainer 2.6.0
newMigration(30, m.migrateDBVersionToDB30),
// Portainer 2.9.0
newMigration(32, m.migrateDBVersionToDB32),
// Portainer 2.9.1, 2.9.2
newMigration(33, m.migrateDBVersionToDB33),
// Portainer 2.10
newMigration(34, m.migrateDBVersionToDB34),
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
newMigration(35, m.migrateDBVersionToDB35),
newMigration(36, m.migrateDBVersionToDB36),
// Portainer 2.13
newMigration(40, m.migrateDBVersionToDB40),
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
// Portainer 2.16
newMigration(70, m.migrateDBVersionToDB70),
// Portainer 2.16.1
newMigration(71, m.migrateDBVersionToDB71),
}
var lastDbVersion int
for _, migration := range migrations {
if m.currentDBVersion < migration.dbversion {
// Print the next line only when the version changes
if migration.dbversion > lastDbVersion {
log.Info().Int("to_version", migration.dbversion).Msg("migrating DB")
}
err := migration.migrate()
if err != nil { if err != nil {
return migrationError(err, GetFunctionName(migration.migrate)) return migrationError(err, "invalid db schema version")
}
}
lastDbVersion = migration.dbversion
} }
log.Info().Int("version", portainer.DBVersion).Msg("setting DB version") newMigratorCount := 0
versionUpdateRequired := false
if schemaVersion.Equal(semver.MustParse(portainer.APIVersion)) {
// detect and run migrations when the versions are the same.
// e.g. development builds
latestMigrations := m.latestMigrations()
if latestMigrations.version.Equal(schemaVersion) &&
version.MigratorCount != len(latestMigrations.migrationFuncs) {
err = m.versionService.StoreDBVersion(portainer.DBVersion) versionUpdateRequired = true
err := runMigrations(latestMigrations.migrationFuncs)
if err != nil {
return err
}
newMigratorCount = len(latestMigrations.migrationFuncs)
}
} else {
// regular path when major/minor/patch versions differ
for _, migration := range m.migrations {
if schemaVersion.LessThan(migration.version) {
versionUpdateRequired = true
log.Info().Msgf("migrating data to %s", migration.version.String())
err := runMigrations(migration.migrationFuncs)
if err != nil {
return err
}
}
newMigratorCount = len(migration.migrationFuncs)
}
}
if versionUpdateRequired || newMigratorCount != version.MigratorCount {
err := m.Always()
if err != nil {
return migrationError(err, "Always migrations returned error")
}
version.SchemaVersion = portainer.APIVersion
version.MigratorCount = newMigratorCount
err = m.versionService.UpdateVersion(version)
if err != nil { if err != nil {
return migrationError(err, "StoreDBVersion") return migrationError(err, "StoreDBVersion")
} }
log.Info().Int("version", portainer.DBVersion).Msg("updated DB version") log.Info().Msgf("db migrated to %s", portainer.APIVersion)
}
// reset DB updating status
return m.versionService.StoreIsUpdating(false) return nil
}
func runMigrations(migrationFuncs []func() error) error {
for _, migrationFunc := range migrationFuncs {
err := migrationFunc()
if err != nil {
return migrationError(err, GetFunctionName(migrationFunc))
}
}
return nil
}
func (m *Migrator) NeedsMigration() bool {
// we need to migrate if anything changes with the version in the DB vs what our software version is.
// If the version matches, then it's all down to the number of migration funcs we have for the current version
// i.e. the MigratorCount
// In this particular instance we should log a fatal error
if m.CurrentDBEdition() != portainer.PortainerCE {
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
return false
}
if m.CurrentSemanticDBVersion().LessThan(semver.MustParse(portainer.APIVersion)) {
return true
}
// Check if we have any migrations for the current version
latestMigrations := m.latestMigrations()
if latestMigrations.version.Equal(semver.MustParse(portainer.APIVersion)) {
if m.currentDBVersion.MigratorCount != len(latestMigrations.migrationFuncs) {
return true
}
} else {
// One remaining possibility if we get here. If our migrator count > 0 and we have no migration funcs
// for the current version (i.e. they were deleted during development). Then we we need to migrate.
// This is to reset the migrator count back to 0
if m.currentDBVersion.MigratorCount > 0 {
return true
}
}
return false
} }

View File

@ -12,7 +12,7 @@ func (m *Migrator) migrateDBVersionToDB34() error {
return MigrateStackEntryPoint(m.stackService) return MigrateStackEntryPoint(m.stackService)
} }
// MigrateStackEntryPoint exported for testing (blah.) // MigrateStackEntryPoint exported for testing
func MigrateStackEntryPoint(stackService dataservices.StackService) error { func MigrateStackEntryPoint(stackService dataservices.StackService) error {
stacks, err := stackService.Stacks() stacks, err := stackService.Stacks()
if err != nil { if err != nil {

View File

@ -1,12 +1,7 @@
package migrator package migrator
import "github.com/rs/zerolog/log"
func (m *Migrator) migrateDBVersionToDB35() error { func (m *Migrator) migrateDBVersionToDB35() error {
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates, // These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
// calling it again will now fix the issue as the function has been repaired. // calling it again will now fix the issue as the function has been repaired.
log.Info().Msg("updating dockerhub registries")
return m.updateDockerhubToDB32() return m.updateDockerhubToDB32()
} }

View File

@ -19,12 +19,13 @@ func (m *Migrator) addGpuInputFieldDB60() error {
} }
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
if endpoint.Gpus == nil {
endpoint.Gpus = []portainer.Pair{} endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil { if err != nil {
return err return err
} }
}
} }
return nil return nil

View File

@ -7,7 +7,7 @@ import (
) )
func (m *Migrator) migrateDBVersionToDB70() error { func (m *Migrator) migrateDBVersionToDB70() error {
log.Info().Msg("- add IngressAvailabilityPerNamespace field") log.Info().Msg("add IngressAvailabilityPerNamespace field")
if err := m.updateIngressFieldsForEnvDB70(); err != nil { if err := m.updateIngressFieldsForEnvDB70(); err != nil {
return err return err
} }

View File

@ -1,7 +1,13 @@
package migrator package migrator
import ( import (
"errors"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/dockerhub" "github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/endpoint" "github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup" "github.com/portainer/portainer/api/dataservices/endpointgroup"
@ -25,7 +31,9 @@ import (
type ( type (
// Migrator defines a service to migrate data after a Portainer version update. // Migrator defines a service to migrate data after a Portainer version update.
Migrator struct { Migrator struct {
currentDBVersion int currentDBVersion *models.Version
migrations []Migrations
endpointGroupService *endpointgroup.Service endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service endpointRelationService *endpointrelation.Service
@ -49,7 +57,7 @@ type (
// MigratorParameters represents the required parameters to create a new Migrator instance. // MigratorParameters represents the required parameters to create a new Migrator instance.
MigratorParameters struct { MigratorParameters struct {
DatabaseVersion int CurrentDBVersion *models.Version
EndpointGroupService *endpointgroup.Service EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service EndpointRelationService *endpointrelation.Service
@ -74,8 +82,8 @@ type (
// NewMigrator creates a new Migrator. // NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator { func NewMigrator(parameters *MigratorParameters) *Migrator {
return &Migrator{ migrator := &Migrator{
currentDBVersion: parameters.DatabaseVersion, currentDBVersion: parameters.CurrentDBVersion,
endpointGroupService: parameters.EndpointGroupService, endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService, endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService, endpointRelationService: parameters.EndpointRelationService,
@ -96,9 +104,112 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
authorizationService: parameters.AuthorizationService, authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService, dockerhubService: parameters.DockerhubService,
} }
migrator.initMigrations()
return migrator
} }
// Version exposes version of database func (m *Migrator) CurrentDBVersion() string {
func (migrator *Migrator) Version() int { return m.currentDBVersion.SchemaVersion
return migrator.currentDBVersion }
func (m *Migrator) CurrentDBEdition() portainer.SoftwareEdition {
return portainer.SoftwareEdition(m.currentDBVersion.Edition)
}
func (m *Migrator) CurrentSemanticDBVersion() *semver.Version {
v, err := semver.NewVersion(m.currentDBVersion.SchemaVersion)
if err != nil {
log.Fatal().Stack().Err(err).Msg("failed to parse current version")
}
return v
}
func (m *Migrator) addMigrations(v string, funcs ...func() error) {
m.migrations = append(m.migrations, Migrations{
version: semver.MustParse(v),
migrationFuncs: funcs,
})
}
func (m *Migrator) latestMigrations() Migrations {
return m.migrations[len(m.migrations)-1]
}
// !NOTE: Migration funtions should ideally be idempotent.
// ! Which simply means the function can run over the same data many times but only transform it once.
// ! In practice this really just means an extra check or two to ensure we're not destroying valid data.
// ! This is not a hard rule though. Understand the limitations. A migration function may only run over
// ! the data more than once if a new migration function is added and the version of your database schema is
// ! the same. e.g. two developers working on the same version add two different functions for different things.
// ! This increases the migration funcs count and so they all run again.
type Migrations struct {
version *semver.Version
migrationFuncs MigrationFuncs
}
type MigrationFuncs []func() error
func (m *Migrator) initMigrations() {
// !IMPORTANT: Do not be tempted to alter the order of these migrations.
// ! Even though one of them looks out of order. Caused by history related
// ! to maintaining two versions and releasing at different times
m.addMigrations("1.0.0", dbTooOldError) // default version found after migration
m.addMigrations("1.21",
m.updateUsersToDBVersion18,
m.updateEndpointsToDBVersion18,
m.updateEndpointGroupsToDBVersion18,
m.updateRegistriesToDBVersion18)
m.addMigrations("1.22", m.updateSettingsToDBVersion19)
m.addMigrations("1.22.1",
m.updateUsersToDBVersion20,
m.updateSettingsToDBVersion20,
m.updateSchedulesToDBVersion20)
m.addMigrations("1.23",
m.updateResourceControlsToDBVersion22,
m.updateUsersAndRolesToDBVersion22)
m.addMigrations("1.24",
m.updateTagsToDBVersion23,
m.updateEndpointsAndEndpointGroupsToDBVersion23)
m.addMigrations("1.24.1", m.updateSettingsToDB24)
m.addMigrations("2.0",
m.updateSettingsToDB25,
m.updateStacksToDB24)
m.addMigrations("2.1", m.updateEndpointSettingsToDB25)
m.addMigrations("2.2", m.updateStackResourceControlToDB27)
m.addMigrations("2.6", m.migrateDBVersionToDB30)
m.addMigrations("2.9", m.migrateDBVersionToDB32)
m.addMigrations("2.9.2", m.migrateDBVersionToDB33)
m.addMigrations("2.10.0", m.migrateDBVersionToDB34)
m.addMigrations("2.9.3", m.migrateDBVersionToDB35)
m.addMigrations("2.12", m.migrateDBVersionToDB36)
m.addMigrations("2.13", m.migrateDBVersionToDB40)
m.addMigrations("2.14", m.migrateDBVersionToDB50)
m.addMigrations("2.15", m.migrateDBVersionToDB60)
m.addMigrations("2.16", m.migrateDBVersionToDB70)
m.addMigrations("2.16.1", m.migrateDBVersionToDB71)
// Add new migrations below...
// One function per migration, each versions migration funcs in the same file.
}
// Always is always run at the end of migrations
func (m *Migrator) Always() error {
// currently nothing to be done in CE... yet
return nil
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support")
} }

View File

@ -4,9 +4,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strconv"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/dataservices/apikeyrepository" "github.com/portainer/portainer/api/dataservices/apikeyrepository"
"github.com/portainer/portainer/api/dataservices/customtemplate" "github.com/portainer/portainer/api/dataservices/customtemplate"
@ -395,7 +395,7 @@ type storeExport struct {
Team []portainer.Team `json:"teams,omitempty"` Team []portainer.Team `json:"teams,omitempty"`
TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitempty"` TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitempty"`
User []portainer.User `json:"users,omitempty"` User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"` Version models.Version `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"` Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"`
} }
@ -588,14 +588,12 @@ func (store *Store) Export(filename string) (err error) {
backup.Webhook = webhooks backup.Webhook = webhooks
} }
v, err := store.Version().DBVersion() if version, err := store.Version().Version(); err != nil {
if err != nil && !store.IsErrObjectNotFound(err) { if !store.IsErrObjectNotFound(err) {
log.Error().Err(err).Msg("exporting DB version") log.Error().Err(err).Msg("exporting Version")
} }
instance, _ := store.Version().InstanceID() } else {
backup.Version = map[string]string{ backup.Version = *version
"DB_VERSION": strconv.Itoa(v),
"INSTANCE_ID": instance,
} }
backup.Metadata, err = store.connection.BackupMetadata() backup.Metadata, err = store.connection.BackupMetadata()
@ -622,19 +620,7 @@ func (store *Store) Import(filename string) (err error) {
return err return err
} }
// TODO: yup, this is bad, and should be in a version struct... store.Version().UpdateVersion(&backup.Version)
if dbversion, ok := backup.Version["DB_VERSION"]; ok {
if v, err := strconv.Atoi(dbversion); err == nil {
if err := store.Version().StoreDBVersion(v); err != nil {
log.Error().Err(err).Msg("DB_VERSION import issue")
}
}
}
if instanceID, ok := backup.Version["INSTANCE_ID"]; ok {
if err := store.Version().StoreInstanceID(instanceID); err != nil {
log.Error().Err(err).Msg("INSTANCE_ID import issue")
}
}
for _, v := range backup.CustomTemplate { for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v) store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)

View File

@ -930,8 +930,6 @@
} }
], ],
"version": { "version": {
"DB_UPDATING": "false", "VERSION": "{\"SchemaVersion\":\"2.17.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"DB_VERSION": "80",
"INSTANCE_ID": "null"
} }
} }

View File

@ -5,6 +5,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database" "github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/filesystem"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -21,8 +22,10 @@ func MustNewTestStore(t *testing.T, init, secure bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(t, init, secure) newStore, store, teardown, err := NewTestStore(t, init, secure)
if err != nil { if err != nil {
if !errors.Is(err, errTempDir) { if !errors.Is(err, errTempDir) {
if teardown != nil {
teardown() teardown()
} }
}
log.Fatal().Err(err).Msg("") log.Fatal().Err(err).Msg("")
} }
@ -33,6 +36,7 @@ func MustNewTestStore(t *testing.T, init, secure bool) (bool, *Store, func()) {
func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error) { func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner. // Creates unique temp directory in a concurrency friendly manner.
storePath := t.TempDir() storePath := t.TempDir()
fileService, err := filesystem.NewService(storePath, "") fileService, err := filesystem.NewService(storePath, "")
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, nil, err
@ -54,8 +58,6 @@ func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error)
return newStore, nil, nil, err return newStore, nil, nil, err
} }
log.Debug().Msg("opened")
if init { if init {
err = store.Init() err = store.Init()
if err != nil { if err != nil {
@ -63,11 +65,13 @@ func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error)
} }
} }
log.Debug().Msg("initialised")
if newStore { if newStore {
// from MigrateData // from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion) v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
}
err = store.VersionService.UpdateVersion(&v)
if err != nil { if err != nil {
return newStore, nil, nil, err return newStore, nil, nil, err
} }

View File

@ -3,6 +3,7 @@ module github.com/portainer/portainer/api
go 1.18 go 1.18
require ( require (
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.5.1 github.com/Microsoft/go-winio v0.5.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/aws/aws-sdk-go-v2 v1.11.1 github.com/aws/aws-sdk-go-v2 v1.11.1
@ -21,6 +22,7 @@ require (
github.com/gofrs/uuid v4.0.0+incompatible github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt/v4 v4.2.0 github.com/golang-jwt/jwt/v4 v4.2.0
github.com/google/go-cmp v0.5.8 github.com/google/go-cmp v0.5.8
github.com/google/uuid v1.1.2
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1

View File

@ -42,6 +42,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
@ -235,6 +237,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=

View File

@ -3,7 +3,6 @@ package status
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
@ -45,9 +44,10 @@ type BuildInfo struct {
// @success 200 {object} versionResponse "Success" // @success 200 {object} versionResponse "Success"
// @router /status/version [get] // @router /status/version [get]
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) { func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
result := &versionResponse{ result := &versionResponse{
ServerVersion: portainer.APIVersion, ServerVersion: portainer.APIVersion,
DatabaseVersion: strconv.Itoa(portainer.DBVersion), DatabaseVersion: portainer.APIVersion,
Build: BuildInfo{ Build: BuildInfo{
BuildNumber: build.BuildNumber, BuildNumber: build.BuildNumber,
ImageTag: build.ImageTag, ImageTag: build.ImageTag,

View File

@ -1453,8 +1453,8 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API // APIVersion is the version number of the Portainer API
APIVersion = "2.17.0" APIVersion = "2.17.0"
// DBVersion is the version number of the Portainer database // Edition is what this edition of Portainer is called
DBVersion = 80 Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9" ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server // AssetsServerURL represents the URL of the Portainer asset server