1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-09 15:55:23 +02:00
This commit is contained in:
Prabhat Khera 2023-04-03 09:01:10 +12:00
parent ecf7f7ec14
commit 525efdc035
29 changed files with 1563 additions and 1749 deletions

View file

@ -1,9 +1,5 @@
package portainer
import (
"io"
)
type ReadTransaction interface {
GetObject(bucketName string, key []byte, object interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
@ -25,19 +21,11 @@ type Transaction interface {
}
type Connection interface {
Transaction
Open() error
Close() error
UpdateTx(fn func(Transaction) error) error
ViewTx(fn func(Transaction) error) error
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
Init() error
// TODO: this one is very database specific atm
BackupTo(w io.Writer) error
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetStorePath() string
@ -45,10 +33,4 @@ type Connection interface {
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
ConvertToKey(v int) []byte
}

View file

@ -4,13 +4,13 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/sqlite"
)
// NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
if storeType == "boltdb" {
return &boltdb.DbConnection{
return &sqlite.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,
}, nil

169
api/database/sqlite/db.go Normal file
View file

@ -0,0 +1,169 @@
package sqlite
import (
"errors"
"os"
"path"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
const (
DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb"
)
var (
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
)
type DbConnection struct {
Path string
EncryptionKey []byte
isEncrypted bool
*gorm.DB
}
func (connection *DbConnection) GetDB() *gorm.DB {
if connection.DB == nil {
err := connection.Open()
if err != nil {
panic(err)
}
}
return connection.DB
}
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
return DatabaseFileName
}
// GetDataseFilePath get the path + filename for the database file
func (connection *DbConnection) GetDatabaseFilePath() string {
if connection.IsEncryptedStore() {
return path.Join(connection.Path, EncryptedDatabaseFileName)
}
return path.Join(connection.Path, DatabaseFileName)
}
// GetStorePath get the filename and path for the database file
func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
// Return true if the database is encrypted
func (connection *DbConnection) IsEncryptedStore() bool {
return connection.getEncryptionKey() != nil
}
func (connection *DbConnection) SetEncrypted(encrypted bool) {
connection.isEncrypted = encrypted
}
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb => ERROR Fatal!
// If we have a loaded encryption key, always set encrypted
if connection.EncryptionKey != nil {
connection.SetEncrypted(true)
}
// Check for portainer.db
dbFile := path.Join(connection.Path, DatabaseFileName)
_, err := os.Stat(dbFile)
haveDbFile := err == nil
// Check for portainer.edb
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
_, err = os.Stat(edbFile)
haveEdbFile := err == nil
if haveDbFile && haveEdbFile {
// 7 - encrypted and unencrypted db?
return false, ErrHaveEncryptedAndUnencrypted
}
if haveDbFile && connection.EncryptionKey != nil {
// 3 - needs migration
return true, nil
}
if haveEdbFile && connection.EncryptionKey == nil {
// 2 - encrypted db, but no key?
return false, ErrHaveEncryptedWithNoKey
}
// 1, 4, 5, 6
return false, nil
}
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := gorm.Open(sqlite.Open(databasePath), &gorm.Config{})
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(5)
sqlDB.SetMaxOpenConns(10)
connection.DB = db
return nil
}
func (connection *DbConnection) Close() error {
sqlDB, err := connection.DB.DB()
if err != nil {
return err
}
connection.DB = nil
return sqlDB.Close()
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
}
func (connection *DbConnection) Init() error {
connection.DB.AutoMigrate(&models.Version{})
connection.DB.AutoMigrate(&portainer.Settings{})
return nil
}

View file

@ -1,14 +1,7 @@
package apikeyrepository
import (
"bytes"
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@ -23,10 +16,10 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
return &Service{
connection: connection,
@ -37,90 +30,91 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
var result = make([]portainer.APIKey, 0)
err := service.connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
// err := service.connection.GetAll(
// BucketName,
// &portainer.APIKey{},
// func(obj interface{}) (interface{}, error) {
// record, ok := obj.(*portainer.APIKey)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
// }
if record.UserID == userID {
result = append(result, *record)
}
// if record.UserID == userID {
// result = append(result, *record)
// }
return &portainer.APIKey{}, nil
})
// return &portainer.APIKey{}, nil
// })
return result, err
return result, nil
}
// GetAPIKeyByDigest returns the API key for the associated digest.
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
var k *portainer.APIKey
stop := fmt.Errorf("ok")
err := service.connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
key, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
if bytes.Equal(key.Digest, digest) {
k = key
return nil, stop
}
// var k *portainer.APIKey
// stop := fmt.Errorf("ok")
// err := service.connection.GetAll(
// BucketName,
// &portainer.APIKey{},
// func(obj interface{}) (interface{}, error) {
// key, ok := obj.(*portainer.APIKey)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
// return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
// }
// if bytes.Equal(key.Digest, digest) {
// k = key
// return nil, stop
// }
return &portainer.APIKey{}, nil
})
// return &portainer.APIKey{}, nil
// })
if errors.Is(err, stop) {
return k, nil
}
// if errors.Is(err, stop) {
// return k, nil
// }
if err == nil {
return nil, dserrors.ErrObjectNotFound
}
// if err == nil {
// return nil, dserrors.ErrObjectNotFound
// }
return nil, err
return nil, nil
}
// CreateAPIKey creates a new APIKey object.
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
record.ID = portainer.APIKeyID(id)
// return service.connection.CreateObject(
// BucketName,
// func(id uint64) (int, interface{}) {
// record.ID = portainer.APIKeyID(id)
return int(record.ID), record
},
)
// return int(record.ID), record
// },
// )
return nil
}
// GetAPIKey retrieves an existing APIKey object by api key ID.
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
var key portainer.APIKey
identifier := service.connection.ConvertToKey(int(keyID))
// identifier := service.connection.ConvertToKey(int(keyID))
err := service.connection.GetObject(BucketName, identifier, &key)
if err != nil {
return nil, err
}
// err := service.connection.GetObject(BucketName, identifier, &key)
// if err != nil {
// return nil, err
// }
return &key, nil
}
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
identifier := service.connection.ConvertToKey(int(key.ID))
return service.connection.UpdateObject(BucketName, identifier, key)
// identifier := service.connection.ConvertToKey(int(key.ID))
// return service.connection.UpdateObject(BucketName, identifier, key)
return nil
}
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
return nil
}

View file

@ -1,11 +1,7 @@
package customtemplate
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
@ -24,10 +20,10 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
// err := connection.SetServiceName(BucketName)
// if err != nil {
// return nil, err
// }
return &Service{
connection: connection,
@ -38,56 +34,49 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.connection.GetAll(
BucketName,
&portainer.CustomTemplate{},
func(obj interface{}) (interface{}, error) {
//var tag portainer.Tag
customTemplate, ok := obj.(*portainer.CustomTemplate)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
}
customTemplates = append(customTemplates, *customTemplate)
// err := service.connection.GetAll(
// BucketName,
// &portainer.CustomTemplate{},
// func(obj interface{}) (interface{}, error) {
// //var tag portainer.Tag
// customTemplate, ok := obj.(*portainer.CustomTemplate)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
// return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
// }
// customTemplates = append(customTemplates, *customTemplate)
return &portainer.CustomTemplate{}, nil
})
// return &portainer.CustomTemplate{}, nil
// })
return customTemplates, err
return customTemplates, nil
}
// CustomTemplate returns an custom template by ID.
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
var customTemplate portainer.CustomTemplate
identifier := service.connection.ConvertToKey(int(ID))
// identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &customTemplate)
if err != nil {
return nil, err
}
// err := service.connection.GetObject(BucketName, identifier, &customTemplate)
// if err != nil {
// return nil, err
// }
return &customTemplate, nil
}
// UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, customTemplate)
return nil
}
// DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
return nil
}
// CreateCustomTemplate uses the existing id and saves it.
// TODO: where does the ID come from, and is it safe?
func (service *Service) Create(customTemplate *portainer.CustomTemplate) error {
return service.connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
return nil
}

View file

@ -21,11 +21,6 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@ -35,15 +30,10 @@ func NewService(connection portainer.Connection) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub
err := service.connection.GetObject(BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil {
return nil, err
}
return &dockerhub, nil
}
// UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return service.connection.UpdateObject(BucketName, []byte(dockerHubKey), dockerhub)
return nil
}

View file

@ -18,11 +18,6 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@ -40,10 +35,10 @@ func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups []portainer.EdgeGroup
var err error
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
groups, err = service.Tx(tx).EdgeGroups()
return err
})
// err = service.connection.ViewTx(func(tx portainer.Transaction) error {
// groups, err = service.Tx(tx).EdgeGroups()
// return err
// })
return groups, err
}
@ -53,40 +48,30 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
var group *portainer.EdgeGroup
var err error
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
group, err = service.Tx(tx).EdgeGroup(ID)
return err
})
// err = service.connection.ViewTx(func(tx portainer.Transaction) error {
// group, err = service.Tx(tx).EdgeGroup(ID)
// return err
// })
return group, err
}
// UpdateEdgeGroup updates an edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, group)
return nil
}
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
id := service.connection.ConvertToKey(int(ID))
edgeGroup := &portainer.EdgeGroup{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
updateFunc(edgeGroup)
})
return nil
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEdgeGroup(ID)
})
return nil
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) Create(group *portainer.EdgeGroup) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(group)
})
return nil
}

View file

@ -2,11 +2,8 @@ package edgegroup
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
@ -22,40 +19,33 @@ func (service ServiceTx) BucketName() string {
func (service ServiceTx) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.EdgeGroup{},
func(obj interface{}) (interface{}, error) {
group, ok := obj.(*portainer.EdgeGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
}
groups = append(groups, *group)
// err := service.tx.GetAllWithJsoniter(
// BucketName,
// &portainer.EdgeGroup{},
// func(obj interface{}) (interface{}, error) {
// group, ok := obj.(*portainer.EdgeGroup)
// if !ok {
// log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
// return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
// }
// groups = append(groups, *group)
return &portainer.EdgeGroup{}, nil
})
// return &portainer.EdgeGroup{}, nil
// })
return groups, err
return groups, nil
}
// EdgeGroup returns an Edge group by ID.
func (service ServiceTx) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an edge group.
func (service ServiceTx) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, group)
return nil
}
// UpdateEdgeGroupFunc is a no-op inside a transaction.
@ -65,16 +55,9 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
// DeleteEdgeGroup deletes an Edge group.
func (service ServiceTx) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
return nil
}
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},
)
return nil
}

View file

@ -1,11 +1,7 @@
package edgejob
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
// BucketName represents the name of the bucket where this service stores data.
@ -22,11 +18,6 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
@ -42,72 +33,32 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.connection.GetAll(
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
job, ok := obj.(*portainer.EdgeJob)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
}
edgeJobs = append(edgeJobs, *job)
return &portainer.EdgeJob{}, nil
})
return edgeJobs, err
return edgeJobs, nil
}
// EdgeJob returns an Edge job by ID
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.connection.CreateObjectWithId(
BucketName,
int(edgeJob.ID),
edgeJob,
)
return nil
}
// Deprecated: use UpdateEdgeJobFunc instead
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, edgeJob)
return nil
}
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
id := service.connection.ConvertToKey(int(ID))
edgeJob := &portainer.EdgeJob{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
updateFunc(edgeJob)
})
return nil
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
return nil
}

View file

@ -2,11 +2,8 @@ package edgejob
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
@ -22,33 +19,12 @@ func (service ServiceTx) BucketName() string {
func (service ServiceTx) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
job, ok := obj.(*portainer.EdgeJob)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("failed to convert to EdgeJob object: %s", obj)
}
edgeJobs = append(edgeJobs, *job)
return &portainer.EdgeJob{}, nil
})
return edgeJobs, err
return edgeJobs, nil
}
// EdgeJob returns an Edge job by ID
func (service ServiceTx) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
@ -62,8 +38,7 @@ func (service ServiceTx) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJ
// UpdateEdgeJob updates an edge job
func (service ServiceTx) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, edgeJob)
return nil
}
// UpdateEdgeJobFunc is a no-op inside a transaction.
@ -73,12 +48,5 @@ func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc fu
// DeleteEdgeJob deletes an Edge job
func (service ServiceTx) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
return nil
}

View file

@ -1,12 +1,9 @@
package edgestack
import (
"fmt"
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
// BucketName represents the name of the bucket where this service stores data.
@ -26,11 +23,6 @@ func (service *Service) BucketName() string {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
s := &Service{
connection: connection,
idxVersion: make(map[portainer.EdgeStackID]int),
@ -63,35 +55,12 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.connection.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.EdgeStack{}, nil
})
return stacks, err
return stacks, nil
}
// EdgeStack returns an Edge stack by ID.
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
@ -108,15 +77,6 @@ func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
err := service.connection.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
return err
}
service.mu.Lock()
service.idxVersion[id] = edgeStack.Version
service.cacheInvalidationFn(id)
@ -130,13 +90,6 @@ func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *por
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
return err
}
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
@ -145,18 +98,7 @@ func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *por
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
id := service.connection.ConvertToKey(int(ID))
edgeStack := &portainer.EdgeStack{}
service.mu.Lock()
defer service.mu.Unlock()
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
updateFunc(edgeStack)
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
})
return nil
}
// UpdateEdgeStackFuncTx is a helper function used to call UpdateEdgeStackFunc inside a transaction.
@ -169,21 +111,7 @@ func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
delete(service.idxVersion, ID)
service.cacheInvalidationFn(ID)
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
}

View file

@ -1,11 +1,7 @@
package edgestack
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
@ -21,33 +17,12 @@ func (service ServiceTx) BucketName() string {
func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
return nil, fmt.Errorf("failed to convert to EdgeStack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.EdgeStack{}, nil
})
return stacks, err
return stacks, nil
}
// EdgeStack returns an Edge stack by ID.
func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
@ -65,15 +40,6 @@ func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
err := service.tx.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
return err
}
service.service.mu.Lock()
service.service.idxVersion[id] = edgeStack.Version
service.service.cacheInvalidationFn(id)
@ -87,13 +53,6 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
service.service.mu.Lock()
defer service.service.mu.Unlock()
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
return err
}
service.service.idxVersion[ID] = edgeStack.Version
service.service.cacheInvalidationFn(ID)
@ -116,22 +75,5 @@ func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFun
func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
service.service.mu.Lock()
defer service.service.mu.Unlock()
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
delete(service.service.idxVersion, ID)
service.service.cacheInvalidationFn(ID)
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View file

@ -60,33 +60,18 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// Endpoint returns an environment(endpoint) by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint *portainer.Endpoint
var err error
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoint, err = service.Tx(tx).Endpoint(ID)
return err
})
if err != nil {
return nil, err
}
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
return endpoint, nil
}
// UpdateEndpoint updates an environment(endpoint).
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
})
return nil
}
// DeleteEndpoint deletes an environment(endpoint).
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEndpoint(ID)
})
return nil
}
func (service *Service) endpoints() ([]portainer.Endpoint, error) {
@ -139,20 +124,5 @@ func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service *Service) Create(endpoint *portainer.Endpoint) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(endpoint)
})
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
var identifier int
service.connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = service.Tx(tx).GetNextIdentifier()
return nil
})
return identifier
return nil
}

View file

@ -1,189 +1,189 @@
package datastore
import (
"fmt"
"os"
"path"
"time"
// import (
// "fmt"
// "os"
// "path"
// "time"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
// "github.com/portainer/portainer/api/database/models"
// "github.com/rs/zerolog/log"
// )
var backupDefaults = struct {
backupDir string
commonDir string
}{
"backups",
"common",
}
// var backupDefaults = struct {
// backupDir string
// commonDir string
// }{
// "backups",
// "common",
// }
//
// Backup Helpers
//
// //
// // Backup Helpers
// //
// createBackupFolders create initial folders for backups
func (store *Store) createBackupFolders() {
// create common dir
commonDir := store.commonBackupDir()
if exists, _ := store.fileService.FileExists(commonDir); !exists {
if err := os.MkdirAll(commonDir, 0700); err != nil {
log.Error().Err(err).Msg("error while creating common backup folder")
}
}
}
// // createBackupFolders create initial folders for backups
// func (store *Store) createBackupFolders() {
// // create common dir
// commonDir := store.commonBackupDir()
// if exists, _ := store.fileService.FileExists(commonDir); !exists {
// if err := os.MkdirAll(commonDir, 0700); err != nil {
// log.Error().Err(err).Msg("error while creating common backup folder")
// }
// }
// }
func (store *Store) databasePath() string {
return store.connection.GetDatabaseFilePath()
}
// func (store *Store) databasePath() string {
// return store.connection.GetDatabaseFilePath()
// }
func (store *Store) commonBackupDir() string {
return path.Join(store.connection.GetStorePath(), backupDefaults.backupDir, backupDefaults.commonDir)
}
// func (store *Store) 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")
// 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")
}
// err := store.fileService.Copy(from, to, true)
// if err != nil {
// log.Error().Err(err).Msg("failed")
// }
return err
}
// return err
// }
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Version string
BackupDir string
BackupFileName string
BackupPath string
}
// // 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,
}
}
// // 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)
}
// // 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,
})
}
// 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
}
// 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")
// // BackupWithOptions backup current database with options
// func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
// log.Info().Msg("creating DB backup")
store.createBackupFolders()
// store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
// 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.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.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,
)
}
// if _, err := store.Open(); err != nil {
// return options.BackupPath, fmt.Errorf(
// "error opening datastore after creating backup: %w",
// err,
// )
// }
return options.BackupPath, nil
}
// 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)
// // 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")
// // 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
}
// return err
// }
err = store.Close()
if err != nil {
log.Error().Err(err).Msg("error while closing store before restore")
// err = store.Close()
// if err != nil {
// log.Error().Err(err).Msg("error while closing store before restore")
return err
}
// return err
// }
log.Info().Msg("restoring DB backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
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
}
// _, 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")
// // 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)
// 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
}
// 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
}
// 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
}
// return nil
// }

View file

@ -1,107 +1,107 @@
package datastore
import (
"fmt"
"os"
"path"
"testing"
// import (
// "fmt"
// "os"
// "path"
// "testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
)
// portainer "github.com/portainer/portainer/api"
// "github.com/portainer/portainer/api/database/models"
// )
func TestCreateBackupFolders(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
// func TestCreateBackupFolders(t *testing.T) {
// _, store := MustNewTestStore(t, true, true)
connection := store.GetConnection()
backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir)
// connection := store.GetConnection()
// backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir)
if isFileExist(backupPath) {
t.Error("Expect backups folder to not exist")
}
// if isFileExist(backupPath) {
// t.Error("Expect backups folder to not exist")
// }
store.createBackupFolders()
if !isFileExist(backupPath) {
t.Error("Expect backups folder to exist")
}
}
// store.createBackupFolders()
// if !isFileExist(backupPath) {
// t.Error("Expect backups folder to exist")
// }
// }
func TestStoreCreation(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
if store == nil {
t.Error("Expect to create a store")
}
// func TestStoreCreation(t *testing.T) {
// _, store := MustNewTestStore(t, true, true)
// if store == nil {
// t.Error("Expect to create a store")
// }
if store.CheckCurrentEdition() != nil {
t.Error("Expect to get CE Edition")
}
}
// if store.CheckCurrentEdition() != nil {
// t.Error("Expect to get CE Edition")
// }
// }
func TestBackup(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
connection := store.GetConnection()
// func TestBackup(t *testing.T) {
// _, store := MustNewTestStore(t, true, true)
// connection := store.GetConnection()
t.Run("Backup should create default db backup", func(t *testing.T) {
v := models.Version{
SchemaVersion: portainer.APIVersion,
}
store.VersionService.UpdateVersion(&v)
store.backupWithOptions(nil)
// t.Run("Backup should create default db backup", func(t *testing.T) {
// v := models.Version{
// SchemaVersion: portainer.APIVersion,
// }
// store.VersionService.UpdateVersion(&v)
// 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)
}
})
// backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion))
// if !isFileExist(backupFileName) {
// t.Errorf("Expect backup file to be created %s", backupFileName)
// }
// })
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
store.backupWithOptions(&BackupOptions{
BackupFileName: beforePortainerVersionUpgradeBackup,
BackupDir: store.commonBackupDir(),
})
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", beforePortainerVersionUpgradeBackup)
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
})
}
// t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
// store.backupWithOptions(&BackupOptions{
// BackupFileName: beforePortainerVersionUpgradeBackup,
// BackupDir: store.commonBackupDir(),
// })
// backupFileName := path.Join(connection.GetStorePath(), "backups", "common", beforePortainerVersionUpgradeBackup)
// if !isFileExist(backupFileName) {
// t.Errorf("Expect backup file to be created %s", backupFileName)
// }
// })
// }
func TestRemoveWithOptions(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
// func TestRemoveWithOptions(t *testing.T) {
// _, store := MustNewTestStore(t, true, true)
t.Run("successfully removes file if existent", func(t *testing.T) {
store.createBackupFolders()
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
// t.Run("successfully removes file if existent", func(t *testing.T) {
// store.createBackupFolders()
// options := &BackupOptions{
// BackupDir: store.commonBackupDir(),
// BackupFileName: "test.txt",
// }
filePath := path.Join(options.BackupDir, options.BackupFileName)
f, err := os.Create(filePath)
if err != nil {
t.Fatalf("file should be created; err=%s", err)
}
f.Close()
// filePath := path.Join(options.BackupDir, options.BackupFileName)
// f, err := os.Create(filePath)
// if err != nil {
// t.Fatalf("file should be created; err=%s", err)
// }
// f.Close()
err = store.removeWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
}
// err = store.removeWithOptions(options)
// if err != nil {
// t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
// }
if isFileExist(f.Name()) {
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
}
})
// if isFileExist(f.Name()) {
// t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
// }
// })
t.Run("fails to removes file if non-existent", func(t *testing.T) {
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
// t.Run("fails to removes file if non-existent", func(t *testing.T) {
// options := &BackupOptions{
// BackupDir: store.commonBackupDir(),
// BackupFileName: "test.txt",
// }
err := store.removeWithOptions(options)
if err == nil {
t.Error("RemoveWithOptions should fail for non-existent file")
}
})
}
// err := store.removeWithOptions(options)
// if err == nil {
// t.Error("RemoveWithOptions should fail for non-existent file")
// }
// })
// }

View file

@ -3,13 +3,11 @@ package datastore
import (
"errors"
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
@ -63,30 +61,6 @@ func (store *Store) Close() error {
return store.connection.Close()
}
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.ViewTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (store *Store) BackupTo(w io.Writer) error {
return store.connection.BackupTo(w)
}
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.Edition {
@ -113,7 +87,7 @@ func (store *Store) Connection() portainer.Connection {
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
return nil
}
func (store *Store) encryptDB() error {
@ -133,7 +107,7 @@ func (store *Store) encryptDB() error {
log.Info().Msg("encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
exportFilename := path.Join(store.connection.GetDatabaseFilePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
log.Info().Str("filename", exportFilename).Msg("exporting database backup")

View file

@ -1,416 +1,416 @@
package datastore
import (
"fmt"
"runtime"
"strings"
"testing"
"github.com/dchest/uniuri"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/crypto"
"github.com/stretchr/testify/assert"
)
const (
adminUsername = "admin"
adminPassword = "password"
standardUsername = "standard"
standardPassword = "password"
agentOnDockerEnvironmentUrl = "tcp://192.168.167.207:30775"
edgeAgentOnKubernetesEnvironmentUrl = "tcp://192.168.167.207"
kubernetesLocalEnvironmentUrl = "https://kubernetes.default.svc"
)
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
testCases := map[string]func(t *testing.T){
"User Accounts": func(t *testing.T) {
store.testUserAccounts(t)
},
"Environments": func(t *testing.T) {
store.testEnvironments(t)
},
"Settings": func(t *testing.T) {
store.testSettings(t)
},
"SSL Settings": func(t *testing.T) {
store.testSSLSettings(t)
},
"Tunnel Server": func(t *testing.T) {
store.testTunnelServer(t)
},
"Custom Templates": func(t *testing.T) {
store.testCustomTemplates(t)
},
"Registries": func(t *testing.T) {
store.testRegistries(t)
},
"Resource Control": func(t *testing.T) {
store.testResourceControl(t)
},
"Schedules": func(t *testing.T) {
store.testSchedules(t)
},
"Tags": func(t *testing.T) {
store.testTags(t)
},
// "Test Title": func(t *testing.T) {
// },
}
for name, test := range testCases {
t.Run(name, test)
}
}
func (store *Store) testEnvironments(t *testing.T) {
id := store.CreateEndpoint(t, "local", portainer.KubernetesLocalEnvironment, "", true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
store.CreateEndpointRelation(id)
}
func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, name, URL string, TLS bool) *portainer.Endpoint {
endpoint := &portainer.Endpoint{
ID: id,
Name: name,
URL: URL,
Type: endpointType,
GroupID: portainer.EndpointGroupID(1),
PublicURL: "",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
if TLS {
endpoint.TLSConfig = portainer.TLSConfiguration{
TLS: true,
TLSSkipVerify: true,
}
}
return endpoint
}
func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
}
func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType portainer.EndpointType, URL string, tls bool) portainer.EndpointID {
is := assert.New(t)
var expectedEndpoint *portainer.Endpoint
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
switch endpointType {
case portainer.DockerEnvironment:
if URL == "" {
URL = "unix:///var/run/docker.sock"
if runtime.GOOS == "windows" {
URL = "npipe:////./pipe/docker_engine"
}
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnDockerEnvironment:
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.AgentOnKubernetesEnvironment:
URL = strings.TrimPrefix(URL, "tcp://")
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
case portainer.EdgeAgentOnKubernetesEnvironment:
cs := chisel.NewService(store, nil)
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
expectedEndpoint.EdgeKey = edgeKey
store.testTunnelServer(t)
case portainer.KubernetesLocalEnvironment:
if URL == "" {
URL = kubernetesLocalEnvironmentUrl
}
expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
}
setEndpointAuthorizations(expectedEndpoint)
store.Endpoint().Create(expectedEndpoint)
endpoint, err := store.Endpoint().Endpoint(id)
is.NoError(err, "Endpoint() should not return an error")
is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
return endpoint.ID
}
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
store.EndpointRelation().Create(relation)
}
func (store *Store) testSSLSettings(t *testing.T) {
is := assert.New(t)
ssl := &portainer.SSLSettings{
CertPath: "/data/certs/cert.pem",
HTTPEnabled: true,
KeyPath: "/data/certs/key.pem",
SelfSigned: true,
}
store.SSLSettings().UpdateSettings(ssl)
settings, err := store.SSLSettings().Settings()
is.NoError(err, "Get sslsettings should succeed")
is.Equal(ssl, settings, "Stored SSLSettings should be the same as what is read out")
}
func (store *Store) testTunnelServer(t *testing.T) {
is := assert.New(t)
expectPrivateKeySeed := uniuri.NewLen(16)
err := store.TunnelServer().UpdateInfo(&portainer.TunnelServerInfo{PrivateKeySeed: expectPrivateKeySeed})
is.NoError(err, "UpdateInfo should have succeeded")
serverInfo, err := store.TunnelServer().Info()
is.NoError(err, "Info should have succeeded")
is.Equal(expectPrivateKeySeed, serverInfo.PrivateKeySeed, "hashed passwords should not differ")
}
// add users, read them back and check the details are unchanged
func (store *Store) testUserAccounts(t *testing.T) {
is := assert.New(t)
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "Account failure")
err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "Account failure")
}
// create an account with the provided details
func (store *Store) createAccount(username, password string, role portainer.UserRole) error {
var err error
user := &portainer.User{Username: username, Role: role}
// encrypt the password
cs := &crypto.Service{}
user.Password, err = cs.Hash(password)
if err != nil {
return err
}
err = store.User().Create(user)
if err != nil {
return err
}
return nil
}
func (store *Store) checkAccount(username, expectPassword string, expectRole portainer.UserRole) error {
// Read the account for username. Check password and role is what we expect
user, err := store.User().UserByUsername(username)
if err != nil {
return errors.Wrap(err, "failed to find user")
}
if user.Username != username || user.Role != expectRole {
return fmt.Errorf("%s user details do not match", user.Username)
}
// Check the password
cs := &crypto.Service{}
expectPasswordHash, err := cs.Hash(expectPassword)
if err != nil {
return errors.Wrap(err, "hash failed")
}
if user.Password != expectPasswordHash {
return fmt.Errorf("%s user password hash failure", user.Username)
}
return nil
}
func (store *Store) testSettings(t *testing.T) {
is := assert.New(t)
// since many settings are default and basically nil, I'm going to update some and read them back
expectedSettings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
expectedSettings.TemplatesURL = "http://portainer.io/application-templates"
expectedSettings.HelmRepositoryURL = "http://portainer.io/helm-repository"
expectedSettings.EdgeAgentCheckinInterval = 60
expectedSettings.AuthenticationMethod = portainer.AuthenticationLDAP
expectedSettings.LDAPSettings = portainer.LDAPSettings{
AnonymousMode: true,
StartTLS: true,
AutoCreateUsers: true,
Password: "random",
}
expectedSettings.SnapshotInterval = "10m"
err = store.Settings().UpdateSettings(expectedSettings)
is.NoError(err, "UpdateSettings() should succeed")
settings, err := store.Settings().Settings()
is.NoError(err, "Settings() should not return an error")
is.Equal(expectedSettings, settings, "stored settings should match")
}
func (store *Store) testCustomTemplates(t *testing.T) {
is := assert.New(t)
customTemplate := store.CustomTemplate()
is.NotNil(customTemplate, "customTemplate Service shouldn't be nil")
expectedTemplate := &portainer.CustomTemplate{
ID: portainer.CustomTemplateID(customTemplate.GetNextIdentifier()),
Title: "Custom Title",
Description: "Custom Template Description",
ProjectPath: "/data/custom_template/1",
Note: "A note about this custom template",
EntryPoint: "docker-compose.yaml",
CreatedByUserID: 10,
}
customTemplate.Create(expectedTemplate)
actualTemplate, err := customTemplate.CustomTemplate(expectedTemplate.ID)
is.NoError(err, "CustomTemplate should not return an error")
is.Equal(expectedTemplate, actualTemplate, "expected and actual template do not match")
}
func (store *Store) testRegistries(t *testing.T) {
is := assert.New(t)
regService := store.RegistryService
is.NotNil(regService, "RegistryService shouldn't be nil")
reg1 := &portainer.Registry{
ID: 1,
Type: portainer.DockerHubRegistry,
Name: "Dockerhub Registry Test",
}
reg2 := &portainer.Registry{
ID: 2,
Type: portainer.GitlabRegistry,
Name: "Gitlab Registry Test",
Gitlab: portainer.GitlabRegistryData{
ProjectID: 12345,
InstanceURL: "http://gitlab.com/12345",
ProjectPath: "mytestproject",
},
}
err := regService.Create(reg1)
is.NoError(err)
err = regService.Create(reg2)
is.NoError(err)
actualReg1, err := regService.Registry(reg1.ID)
is.NoError(err)
is.Equal(reg1, actualReg1, "registries differ")
actualReg2, err := regService.Registry(reg2.ID)
is.NoError(err)
is.Equal(reg2, actualReg2, "registries differ")
}
func (store *Store) testResourceControl(t *testing.T) {
// is := assert.New(t)
// resControl := store.ResourceControl()
// ctrl := &portainer.ResourceControl{
// }
// resControl().Create()
}
func (store *Store) testSchedules(t *testing.T) {
is := assert.New(t)
schedule := store.ScheduleService
s := &portainer.Schedule{
ID: portainer.ScheduleID(schedule.GetNextIdentifier()),
Name: "My Custom Schedule 1",
CronExpression: "*/5 * * * * portainer /bin/sh -c echo 'hello world'",
}
err := schedule.CreateSchedule(s)
is.NoError(err, "CreateSchedule should succeed")
actual, err := schedule.Schedule(s.ID)
is.NoError(err, "schedule should be found")
is.Equal(s, actual, "schedules differ")
}
func (store *Store) testTags(t *testing.T) {
is := assert.New(t)
tags := store.TagService
tag1 := &portainer.Tag{
ID: 1,
Name: "Tag 1",
}
tag2 := &portainer.Tag{
ID: 2,
Name: "Tag 1",
}
err := tags.Create(tag1)
is.NoError(err, "Tags.Create should succeed")
err = tags.Create(tag2)
is.NoError(err, "Tags.Create should succeed")
actual, err := tags.Tag(tag1.ID)
is.NoError(err, "tag1 should be found")
is.Equal(tag1, actual, "tags differ")
actual, err = tags.Tag(tag2.ID)
is.NoError(err, "tag2 should be found")
is.Equal(tag2, actual, "tags differ")
}
// import (
// "fmt"
// "runtime"
// "strings"
// "testing"
// "github.com/dchest/uniuri"
// "github.com/pkg/errors"
// portainer "github.com/portainer/portainer/api"
// "github.com/portainer/portainer/api/chisel"
// "github.com/portainer/portainer/api/crypto"
// "github.com/stretchr/testify/assert"
// )
// const (
// adminUsername = "admin"
// adminPassword = "password"
// standardUsername = "standard"
// standardPassword = "password"
// agentOnDockerEnvironmentUrl = "tcp://192.168.167.207:30775"
// edgeAgentOnKubernetesEnvironmentUrl = "tcp://192.168.167.207"
// kubernetesLocalEnvironmentUrl = "https://kubernetes.default.svc"
// )
// // TestStoreFull an eventually comprehensive set of tests for the Store.
// // The idea is what we write to the store, we should read back.
// func TestStoreFull(t *testing.T) {
// _, store := MustNewTestStore(t, true, true)
// testCases := map[string]func(t *testing.T){
// "User Accounts": func(t *testing.T) {
// store.testUserAccounts(t)
// },
// "Environments": func(t *testing.T) {
// store.testEnvironments(t)
// },
// "Settings": func(t *testing.T) {
// store.testSettings(t)
// },
// "SSL Settings": func(t *testing.T) {
// store.testSSLSettings(t)
// },
// "Tunnel Server": func(t *testing.T) {
// store.testTunnelServer(t)
// },
// "Custom Templates": func(t *testing.T) {
// store.testCustomTemplates(t)
// },
// "Registries": func(t *testing.T) {
// store.testRegistries(t)
// },
// "Resource Control": func(t *testing.T) {
// store.testResourceControl(t)
// },
// "Schedules": func(t *testing.T) {
// store.testSchedules(t)
// },
// "Tags": func(t *testing.T) {
// store.testTags(t)
// },
// // "Test Title": func(t *testing.T) {
// // },
// }
// for name, test := range testCases {
// t.Run(name, test)
// }
// }
// func (store *Store) testEnvironments(t *testing.T) {
// id := store.CreateEndpoint(t, "local", portainer.KubernetesLocalEnvironment, "", true)
// store.CreateEndpointRelation(id)
// id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
// store.CreateEndpointRelation(id)
// id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
// store.CreateEndpointRelation(id)
// }
// func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, name, URL string, TLS bool) *portainer.Endpoint {
// endpoint := &portainer.Endpoint{
// ID: id,
// Name: name,
// URL: URL,
// Type: endpointType,
// GroupID: portainer.EndpointGroupID(1),
// PublicURL: "",
// TLSConfig: portainer.TLSConfiguration{
// TLS: false,
// },
// UserAccessPolicies: portainer.UserAccessPolicies{},
// TeamAccessPolicies: portainer.TeamAccessPolicies{},
// TagIDs: []portainer.TagID{},
// Status: portainer.EndpointStatusUp,
// Snapshots: []portainer.DockerSnapshot{},
// Kubernetes: portainer.KubernetesDefault(),
// }
// if TLS {
// endpoint.TLSConfig = portainer.TLSConfiguration{
// TLS: true,
// TLSSkipVerify: true,
// }
// }
// return endpoint
// }
// func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
// endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
// AllowVolumeBrowserForRegularUsers: false,
// EnableHostManagementFeatures: false,
// AllowSysctlSettingForRegularUsers: true,
// AllowBindMountsForRegularUsers: true,
// AllowPrivilegedModeForRegularUsers: true,
// AllowHostNamespaceForRegularUsers: true,
// AllowContainerCapabilitiesForRegularUsers: true,
// AllowDeviceMappingForRegularUsers: true,
// AllowStackManagementForRegularUsers: true,
// }
// }
// func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType portainer.EndpointType, URL string, tls bool) portainer.EndpointID {
// is := assert.New(t)
// var expectedEndpoint *portainer.Endpoint
// id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
// switch endpointType {
// case portainer.DockerEnvironment:
// if URL == "" {
// URL = "unix:///var/run/docker.sock"
// if runtime.GOOS == "windows" {
// URL = "npipe:////./pipe/docker_engine"
// }
// }
// expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
// case portainer.AgentOnDockerEnvironment:
// expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
// case portainer.AgentOnKubernetesEnvironment:
// URL = strings.TrimPrefix(URL, "tcp://")
// expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
// case portainer.EdgeAgentOnKubernetesEnvironment:
// cs := chisel.NewService(store, nil)
// expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
// edgeKey := cs.GenerateEdgeKey(URL, "", int(id))
// expectedEndpoint.EdgeKey = edgeKey
// store.testTunnelServer(t)
// case portainer.KubernetesLocalEnvironment:
// if URL == "" {
// URL = kubernetesLocalEnvironmentUrl
// }
// expectedEndpoint = newEndpoint(endpointType, id, name, URL, tls)
// }
// setEndpointAuthorizations(expectedEndpoint)
// store.Endpoint().Create(expectedEndpoint)
// endpoint, err := store.Endpoint().Endpoint(id)
// is.NoError(err, "Endpoint() should not return an error")
// is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
// return endpoint.ID
// }
// func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
// relation := &portainer.EndpointRelation{
// EndpointID: id,
// EdgeStacks: map[portainer.EdgeStackID]bool{},
// }
// store.EndpointRelation().Create(relation)
// }
// func (store *Store) testSSLSettings(t *testing.T) {
// is := assert.New(t)
// ssl := &portainer.SSLSettings{
// CertPath: "/data/certs/cert.pem",
// HTTPEnabled: true,
// KeyPath: "/data/certs/key.pem",
// SelfSigned: true,
// }
// store.SSLSettings().UpdateSettings(ssl)
// settings, err := store.SSLSettings().Settings()
// is.NoError(err, "Get sslsettings should succeed")
// is.Equal(ssl, settings, "Stored SSLSettings should be the same as what is read out")
// }
// func (store *Store) testTunnelServer(t *testing.T) {
// is := assert.New(t)
// expectPrivateKeySeed := uniuri.NewLen(16)
// err := store.TunnelServer().UpdateInfo(&portainer.TunnelServerInfo{PrivateKeySeed: expectPrivateKeySeed})
// is.NoError(err, "UpdateInfo should have succeeded")
// serverInfo, err := store.TunnelServer().Info()
// is.NoError(err, "Info should have succeeded")
// is.Equal(expectPrivateKeySeed, serverInfo.PrivateKeySeed, "hashed passwords should not differ")
// }
// // add users, read them back and check the details are unchanged
// func (store *Store) testUserAccounts(t *testing.T) {
// is := assert.New(t)
// err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
// is.NoError(err, "CreateAccount should succeed")
// store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
// is.NoError(err, "Account failure")
// err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
// is.NoError(err, "CreateAccount should succeed")
// store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
// is.NoError(err, "Account failure")
// }
// // create an account with the provided details
// func (store *Store) createAccount(username, password string, role portainer.UserRole) error {
// var err error
// user := &portainer.User{Username: username, Role: role}
// // encrypt the password
// cs := &crypto.Service{}
// user.Password, err = cs.Hash(password)
// if err != nil {
// return err
// }
// err = store.User().Create(user)
// if err != nil {
// return err
// }
// return nil
// }
// func (store *Store) checkAccount(username, expectPassword string, expectRole portainer.UserRole) error {
// // Read the account for username. Check password and role is what we expect
// user, err := store.User().UserByUsername(username)
// if err != nil {
// return errors.Wrap(err, "failed to find user")
// }
// if user.Username != username || user.Role != expectRole {
// return fmt.Errorf("%s user details do not match", user.Username)
// }
// // Check the password
// cs := &crypto.Service{}
// expectPasswordHash, err := cs.Hash(expectPassword)
// if err != nil {
// return errors.Wrap(err, "hash failed")
// }
// if user.Password != expectPasswordHash {
// return fmt.Errorf("%s user password hash failure", user.Username)
// }
// return nil
// }
// func (store *Store) testSettings(t *testing.T) {
// is := assert.New(t)
// // since many settings are default and basically nil, I'm going to update some and read them back
// expectedSettings, err := store.Settings().Settings()
// is.NoError(err, "Settings() should not return an error")
// expectedSettings.TemplatesURL = "http://portainer.io/application-templates"
// expectedSettings.HelmRepositoryURL = "http://portainer.io/helm-repository"
// expectedSettings.EdgeAgentCheckinInterval = 60
// expectedSettings.AuthenticationMethod = portainer.AuthenticationLDAP
// expectedSettings.LDAPSettings = portainer.LDAPSettings{
// AnonymousMode: true,
// StartTLS: true,
// AutoCreateUsers: true,
// Password: "random",
// }
// expectedSettings.SnapshotInterval = "10m"
// err = store.Settings().UpdateSettings(expectedSettings)
// is.NoError(err, "UpdateSettings() should succeed")
// settings, err := store.Settings().Settings()
// is.NoError(err, "Settings() should not return an error")
// is.Equal(expectedSettings, settings, "stored settings should match")
// }
// func (store *Store) testCustomTemplates(t *testing.T) {
// is := assert.New(t)
// customTemplate := store.CustomTemplate()
// is.NotNil(customTemplate, "customTemplate Service shouldn't be nil")
// expectedTemplate := &portainer.CustomTemplate{
// ID: portainer.CustomTemplateID(customTemplate.GetNextIdentifier()),
// Title: "Custom Title",
// Description: "Custom Template Description",
// ProjectPath: "/data/custom_template/1",
// Note: "A note about this custom template",
// EntryPoint: "docker-compose.yaml",
// CreatedByUserID: 10,
// }
// customTemplate.Create(expectedTemplate)
// actualTemplate, err := customTemplate.CustomTemplate(expectedTemplate.ID)
// is.NoError(err, "CustomTemplate should not return an error")
// is.Equal(expectedTemplate, actualTemplate, "expected and actual template do not match")
// }
// func (store *Store) testRegistries(t *testing.T) {
// is := assert.New(t)
// regService := store.RegistryService
// is.NotNil(regService, "RegistryService shouldn't be nil")
// reg1 := &portainer.Registry{
// ID: 1,
// Type: portainer.DockerHubRegistry,
// Name: "Dockerhub Registry Test",
// }
// reg2 := &portainer.Registry{
// ID: 2,
// Type: portainer.GitlabRegistry,
// Name: "Gitlab Registry Test",
// Gitlab: portainer.GitlabRegistryData{
// ProjectID: 12345,
// InstanceURL: "http://gitlab.com/12345",
// ProjectPath: "mytestproject",
// },
// }
// err := regService.Create(reg1)
// is.NoError(err)
// err = regService.Create(reg2)
// is.NoError(err)
// actualReg1, err := regService.Registry(reg1.ID)
// is.NoError(err)
// is.Equal(reg1, actualReg1, "registries differ")
// actualReg2, err := regService.Registry(reg2.ID)
// is.NoError(err)
// is.Equal(reg2, actualReg2, "registries differ")
// }
// func (store *Store) testResourceControl(t *testing.T) {
// // is := assert.New(t)
// // resControl := store.ResourceControl()
// // ctrl := &portainer.ResourceControl{
// // }
// // resControl().Create()
// }
// func (store *Store) testSchedules(t *testing.T) {
// is := assert.New(t)
// schedule := store.ScheduleService
// s := &portainer.Schedule{
// ID: portainer.ScheduleID(schedule.GetNextIdentifier()),
// Name: "My Custom Schedule 1",
// CronExpression: "*/5 * * * * portainer /bin/sh -c echo 'hello world'",
// }
// err := schedule.CreateSchedule(s)
// is.NoError(err, "CreateSchedule should succeed")
// actual, err := schedule.Schedule(s.ID)
// is.NoError(err, "schedule should be found")
// is.Equal(s, actual, "schedules differ")
// }
// func (store *Store) testTags(t *testing.T) {
// is := assert.New(t)
// tags := store.TagService
// tag1 := &portainer.Tag{
// ID: 1,
// Name: "Tag 1",
// }
// tag2 := &portainer.Tag{
// ID: 2,
// Name: "Tag 1",
// }
// err := tags.Create(tag1)
// is.NoError(err, "Tags.Create should succeed")
// err = tags.Create(tag2)
// is.NoError(err, "Tags.Create should succeed")
// actual, err := tags.Tag(tag1.ID)
// is.NoError(err, "tag1 should be found")
// is.Equal(tag1, actual, "tags differ")
// actual, err = tags.Tag(tag2.ID)
// is.NoError(err, "tag2 should be found")
// is.Equal(tag2, actual, "tags differ")
// }

View file

@ -1,145 +1,145 @@
package datastore
import (
"fmt"
"runtime/debug"
// import (
// "fmt"
// "runtime/debug"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli"
"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/internal/authorization"
// portainer "github.com/portainer/portainer/api"
// "github.com/portainer/portainer/api/cli"
// "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/internal/authorization"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// "github.com/pkg/errors"
// "github.com/rs/zerolog/log"
// )
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
// const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
func (store *Store) MigrateData() error {
updating, err := store.VersionService.IsUpdating()
if err != nil {
return errors.Wrap(err, "while checking if the store is updating")
}
// func (store *Store) MigrateData() error {
// updating, err := store.VersionService.IsUpdating()
// if err != nil {
// return errors.Wrap(err, "while checking if the store is updating")
// }
if updating {
return dserrors.ErrDatabaseIsUpdating
}
// if updating {
// return dserrors.ErrDatabaseIsUpdating
// }
// migrate new version bucket if required (doesn't write anything to db yet)
version, err := store.getOrMigrateLegacyVersion()
if err != nil {
return errors.Wrap(err, "while migrating legacy version")
}
// // migrate new version bucket if required (doesn't write anything to db yet)
// version, err := store.getOrMigrateLegacyVersion()
// if err != nil {
// return errors.Wrap(err, "while migrating legacy version")
// }
migratorParams := store.newMigratorParameters(version)
migrator := migrator.NewMigrator(migratorParams)
// migratorParams := store.newMigratorParameters(version)
// migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() {
return nil
}
// 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")
}
// // 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 = errors.Wrap(err, "failed to migrate database")
// err = store.FailSafeMigrate(migrator, version)
// if err != nil {
// err = errors.Wrap(err, "failed to migrate database")
log.Warn().Msg("migration failed, restoring database to previous version")
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")
}
// log.Warn().Msg("migration failed, restoring database to previous version")
// 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 err
}
// log.Info().Msg("database restored to previous version")
// return err
// }
return nil
}
// return nil
// }
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
return &migrator.MigratorParameters{
CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
SnapshotService: store.SnapshotService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
EdgeStackService: store.EdgeStackService,
EdgeJobService: store.EdgeJobService,
}
}
// func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
// return &migrator.MigratorParameters{
// CurrentDBVersion: version,
// EndpointGroupService: store.EndpointGroupService,
// EndpointService: store.EndpointService,
// EndpointRelationService: store.EndpointRelationService,
// ExtensionService: store.ExtensionService,
// RegistryService: store.RegistryService,
// ResourceControlService: store.ResourceControlService,
// RoleService: store.RoleService,
// ScheduleService: store.ScheduleService,
// SettingsService: store.SettingsService,
// SnapshotService: store.SnapshotService,
// StackService: store.StackService,
// TagService: store.TagService,
// TeamMembershipService: store.TeamMembershipService,
// UserService: store.UserService,
// VersionService: store.VersionService,
// FileService: store.fileService,
// DockerhubService: store.DockerHubService,
// AuthorizationService: authorization.NewService(store),
// EdgeStackService: store.EdgeStackService,
// EdgeJobService: store.EdgeJobService,
// }
// }
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
defer func() {
if e := recover(); e != nil {
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
}
}()
// // FailSafeMigrate backup and restore DB if migration fail
// func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
// defer func() {
// if e := recover(); e != nil {
// // return error with cause and stacktrace (recover() doesn't include a stacktrace)
// err = fmt.Errorf("%v %s", e, string(debug.Stack()))
// }
// }()
err = store.VersionService.StoreIsUpdating(true)
if err != nil {
return errors.Wrap(err, "while updating the store")
}
// err = store.VersionService.StoreIsUpdating(true)
// if err != nil {
// return errors.Wrap(err, "while updating the store")
// }
// now update the version to the new struct (if required)
err = store.finishMigrateLegacyVersion(version)
if err != nil {
return errors.Wrap(err, "while updating version")
}
// // now update the version to the new struct (if required)
// err = store.finishMigrateLegacyVersion(version)
// if err != nil {
// return errors.Wrap(err, "while updating version")
// }
log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portainer.APIVersion)
// log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portainer.APIVersion)
err = migrator.Migrate()
if err != nil {
return err
}
// err = migrator.Migrate()
// if err != nil {
// return err
// }
err = store.VersionService.StoreIsUpdating(false)
if err != nil {
return errors.Wrap(err, "failed to update the store")
}
// err = store.VersionService.StoreIsUpdating(false)
// if err != nil {
// return errors.Wrap(err, "failed to update the store")
// }
return nil
}
// return nil
// }
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
func (store *Store) connectionRollback(force bool) error {
// // Rollback to a pre-upgrade backup copy/snapshot of portainer.db
// func (store *Store) connectionRollback(force bool) error {
if !force {
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
if err != nil || !confirmed {
return err
}
}
// if !force {
// confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
// if err != nil || !confirmed {
// return err
// }
// }
options := getBackupRestoreOptions(store.commonBackupDir())
// options := getBackupRestoreOptions(store.commonBackupDir())
err := store.restoreWithOptions(options)
if err != nil {
return err
}
// err := store.restoreWithOptions(options)
// if err != nil {
// return err
// }
return store.connection.Close()
}
// return store.connection.Close()
// }

View file

@ -1,20 +1,9 @@
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/portainer/portainer/api/database/boltdb"
"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
@ -29,31 +18,31 @@ func testVersion(store *Store, versionWant string, t *testing.T) {
}
func TestMigrateData(t *testing.T) {
snapshotTests := []struct {
testName string
srcPath string
wantPath string
overrideInstanceId bool
}{
{
testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
overrideInstanceId: true,
},
}
for _, test := range snapshotTests {
t.Run(test.testName, func(t *testing.T) {
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
if err != nil {
t.Errorf(
"Failed migrating mock database %v: %v",
test.srcPath,
err,
)
}
})
}
// snapshotTests := []struct {
// testName string
// srcPath string
// wantPath string
// overrideInstanceId bool
// }{
// {
// testName: "migrate version 24 to latest",
// srcPath: "test_data/input_24.json",
// wantPath: "test_data/output_24_to_latest.json",
// overrideInstanceId: true,
// },
// }
// for _, test := range snapshotTests {
// t.Run(test.testName, func(t *testing.T) {
// err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
// if err != nil {
// t.Errorf(
// "Failed migrating mock database %v: %v",
// test.srcPath,
// err,
// )
// }
// })
// }
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
// newStore, store, teardown := MustNewTestStore(t, true, false)
@ -163,59 +152,59 @@ func TestMigrateData(t *testing.T) {
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store := MustNewTestStore(t, false, true)
// _, store := MustNewTestStore(t, false, true)
options := getBackupRestoreOptions(store.commonBackupDir())
// options := getBackupRestoreOptions(store.commonBackupDir())
wantDir := store.commonBackupDir()
if !strings.HasSuffix(options.BackupDir, wantDir) {
log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
}
// wantDir := store.commonBackupDir()
// if !strings.HasSuffix(options.BackupDir, wantDir) {
// log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
// }
wantFilename := "portainer.db.bak"
if options.BackupFileName != wantFilename {
log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
}
// wantFilename := "portainer.db.bak"
// if options.BackupFileName != wantFilename {
// log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
// }
}
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := models.Version{SchemaVersion: "2.4.0"}
_, store := MustNewTestStore(t, true, false)
// t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
// version := models.Version{SchemaVersion: "2.4.0"}
// _, store := MustNewTestStore(t, true, false)
err := store.VersionService.UpdateVersion(&version)
if err != nil {
t.Errorf("Failed updating version: %v", err)
}
// err := store.VersionService.UpdateVersion(&version)
// if err != nil {
// t.Errorf("Failed updating version: %v", err)
// }
_, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
if err != nil {
log.Fatal().Err(err).Msg("")
}
// _, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
// if err != nil {
// log.Fatal().Err(err).Msg("")
// }
// Change the current version
version2 := models.Version{SchemaVersion: "2.6.0"}
err = store.VersionService.UpdateVersion(&version2)
if err != nil {
log.Fatal().Err(err).Msg("")
}
// // Change the current version
// version2 := models.Version{SchemaVersion: "2.6.0"}
// err = store.VersionService.UpdateVersion(&version2)
// if err != nil {
// log.Fatal().Err(err).Msg("")
// }
err = store.Rollback(true)
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
// err = store.Rollback(true)
// if err != nil {
// t.Logf("Rollback failed: %s", err)
// t.Fail()
// return
// }
_, err = store.Open()
if err != nil {
t.Logf("Open failed: %s", err)
t.Fail()
return
}
// _, err = store.Open()
// if err != nil {
// t.Logf("Open failed: %s", err)
// t.Fail()
// return
// }
testVersion(store, version.SchemaVersion, t)
})
// testVersion(store, version.SchemaVersion, t)
// })
}
// isFileExist is helper function to check for file existence
@ -231,288 +220,288 @@ func isFileExist(path string) bool {
// parses it into a database, runs a migration on that database, and then
// compares it with an expected output database.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
srcJSON, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
}
// srcJSON, err := os.ReadFile(srcPath)
// if err != nil {
// t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
// }
// Parse source json to db.
// When we create a new test store, it sets its version field automatically to latest.
_, store := MustNewTestStore(t, true, false)
// // Parse source json to db.
// // When we create a new test store, it sets its version field automatically to latest.
// _, store := MustNewTestStore(t, true, false)
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
store.connection.DeleteObject("version", []byte("VERSION"))
// fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
// store.connection.DeleteObject("version", []byte("VERSION"))
// defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
}
// // defer teardown()
// err = importJSON(t, bytes.NewReader(srcJSON), store)
// if err != nil {
// return err
// }
// Run the actual migrations on our input database.
err = store.MigrateData()
if err != nil {
return err
}
// // Run the actual migrations on our input database.
// err = store.MigrateData()
// if err != nil {
// 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
}
// 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
}
}
// 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
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
err = store.connection.Close()
if err != nil {
t.Fatalf("err closing bolt connection: %v", err)
}
con, ok := store.connection.(*boltdb.DbConnection)
if !ok {
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
}
// // Assert that our database connection is using bolt so we can call
// // exportJson rather than ExportRaw. The exportJson function allows us to
// // strip out the metadata which we don't want for our tests.
// // TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
// err = store.connection.Close()
// if err != nil {
// t.Fatalf("err closing bolt connection: %v", err)
// }
// con, ok := store.connection.(*boltdb.DbConnection)
// if !ok {
// t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
// }
// Convert database back to json.
databasePath := con.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %w", databasePath, err)
}
// // Convert database back to json.
// databasePath := con.GetDatabaseFilePath()
// if _, err := os.Stat(databasePath); err != nil {
// return fmt.Errorf("stat on %s failed: %w", databasePath, err)
// }
gotJSON, err := con.ExportJSON(databasePath, false)
if err != nil {
t.Logf(
"failed re-exporting database %s to JSON: %v",
databasePath,
err,
)
}
// gotJSON, err := con.ExportJSON(databasePath, false)
// if err != nil {
// t.Logf(
// "failed re-exporting database %s to JSON: %v",
// databasePath,
// err,
// )
// }
wantJSON, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
}
// wantJSON, err := os.ReadFile(wantPath)
// if err != nil {
// t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
// }
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
0600,
)
t.Errorf(
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
srcPath,
wantPath,
gotPath,
diff,
)
}
// // Compare the result we got with the one we wanted.
// if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
// gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
// os.WriteFile(
// gotPath,
// gotJSON,
// 0600,
// )
// t.Errorf(
// "migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
// srcPath,
// wantPath,
// gotPath,
// diff,
// )
// }
return nil
}
// importJSON reads input JSON and commits it to a portainer datastore.Store.
// Errors are logged with the testing package.
func importJSON(t *testing.T, r io.Reader, store *Store) error {
objects := make(map[string]interface{})
// objects := make(map[string]interface{})
// Parse json into map of objects.
d := json.NewDecoder(r)
d.UseNumber()
err := d.Decode(&objects)
if err != nil {
return err
}
// // Parse json into map of objects.
// d := json.NewDecoder(r)
// d.UseNumber()
// err := d.Decode(&objects)
// if err != nil {
// return err
// }
// Get database connection from store.
con := store.connection
// // Get database connection from store.
// con := store.connection
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed casting %s to map[string]interface{}", k)
}
// for k, v := range objects {
// switch k {
// case "version":
// versions, ok := v.(map[string]interface{})
// if !ok {
// t.Logf("failed casting %s to map[string]interface{}", k)
// }
// New format db
version, ok := versions["VERSION"]
if ok {
err := con.CreateObjectWithStringId(
k,
[]byte("VERSION"),
version,
)
if err != nil {
t.Logf("failed writing VERSION in %s: %v", k, err)
}
}
// // New format db
// version, ok := versions["VERSION"]
// 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
// // old format db
dbVersion, ok := versions["DB_VERSION"]
if ok {
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
// dbVersion, ok := versions["DB_VERSION"]
// if ok {
// numDBVersion, ok := dbVersion.(json.Number)
// if !ok {
// t.Logf("failed parsing DB_VERSION as json number from %s", k)
// }
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
// intDBVersion, err := numDBVersion.Int64()
// if err != nil {
// t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
// }
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
}
}
// err = con.CreateObjectWithStringId(
// k,
// []byte("DB_VERSION"),
// int(intDBVersion),
// )
// if err != nil {
// t.Logf("failed writing DB_VERSION in %s: %v", k, err)
// }
// }
instanceID, ok := versions["INSTANCE_ID"]
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
}
}
// instanceID, ok := versions["INSTANCE_ID"]
// if ok {
// err = con.CreateObjectWithStringId(
// k,
// []byte("INSTANCE_ID"),
// instanceID,
// )
// if err != nil {
// 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)
}
}
// 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":
obj, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("DOCKERHUB"),
obj[0],
)
if err != nil {
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
}
// case "dockerhub":
// obj, ok := v.([]interface{})
// if !ok {
// t.Logf("failed to cast %s to []interface{}", k)
// }
// err := con.CreateObjectWithStringId(
// k,
// []byte("DOCKERHUB"),
// obj[0],
// )
// if err != nil {
// t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
// }
case "ssl":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SSL"),
obj,
)
if err != nil {
t.Logf("failed writing SSL in %s: %v", k, err)
}
// case "ssl":
// obj, ok := v.(map[string]interface{})
// if !ok {
// t.Logf("failed to case %s to map[string]interface{}", k)
// }
// err := con.CreateObjectWithStringId(
// k,
// []byte("SSL"),
// obj,
// )
// if err != nil {
// t.Logf("failed writing SSL in %s: %v", k, err)
// }
case "settings":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SETTINGS"),
obj,
)
if err != nil {
t.Logf("failed writing SETTINGS in %s: %v", k, err)
}
// case "settings":
// obj, ok := v.(map[string]interface{})
// if !ok {
// t.Logf("failed to case %s to map[string]interface{}", k)
// }
// err := con.CreateObjectWithStringId(
// k,
// []byte("SETTINGS"),
// obj,
// )
// if err != nil {
// t.Logf("failed writing SETTINGS in %s: %v", k, err)
// }
case "tunnel_server":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("INFO"),
obj,
)
if err != nil {
t.Logf("failed writing INFO in %s: %v", k, err)
}
case "templates":
continue
// case "tunnel_server":
// obj, ok := v.(map[string]interface{})
// if !ok {
// t.Logf("failed to case %s to map[string]interface{}", k)
// }
// err := con.CreateObjectWithStringId(
// k,
// []byte("INFO"),
// obj,
// )
// if err != nil {
// t.Logf("failed writing INFO in %s: %v", k, err)
// }
// case "templates":
// continue
default:
objlist, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
// default:
// objlist, ok := v.([]interface{})
// if !ok {
// t.Logf("failed to cast %s to []interface{}", k)
// }
for _, obj := range objlist {
value, ok := obj.(map[string]interface{})
if !ok {
t.Logf("failed to cast %v to map[string]interface{}", obj)
} else {
var ok bool
var id interface{}
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird
// stringification
id, ok = value["EndpointID"]
default:
id, ok = value["Id"]
}
if !ok {
// endpoint_relations: EndpointID
t.Logf("missing Id field: %s", k)
id = "error"
}
n, ok := id.(json.Number)
if !ok {
t.Logf("failed to cast %v to json.Number in %s", id, k)
} else {
key, err := n.Int64()
if err != nil {
t.Logf("failed to cast %v to int in %s", n, k)
} else {
err := con.CreateObjectWithId(
k,
int(key),
value,
)
if err != nil {
t.Logf("failed writing %v in %s: %v", key, k, err)
}
}
}
}
}
}
}
// for _, obj := range objlist {
// value, ok := obj.(map[string]interface{})
// if !ok {
// t.Logf("failed to cast %v to map[string]interface{}", obj)
// } else {
// var ok bool
// var id interface{}
// switch k {
// case "endpoint_relations":
// // TODO: need to make into an int, then do that weird
// // stringification
// id, ok = value["EndpointID"]
// default:
// id, ok = value["Id"]
// }
// if !ok {
// // endpoint_relations: EndpointID
// t.Logf("missing Id field: %s", k)
// id = "error"
// }
// n, ok := id.(json.Number)
// if !ok {
// t.Logf("failed to cast %v to json.Number in %s", id, k)
// } else {
// key, err := n.Int64()
// if err != nil {
// t.Logf("failed to cast %v to int in %s", n, k)
// } else {
// err := con.CreateObjectWithId(
// k,
// int(key),
// value,
// )
// if err != nil {
// t.Logf("failed writing %v in %s: %v", key, k, err)
// }
// }
// }
// }
// }
// }
// }
return nil
}

View file

@ -1,89 +1,89 @@
package datastore
import (
"testing"
// import (
// "testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
)
// portainer "github.com/portainer/portainer/api"
// "github.com/portainer/portainer/api/datastore/migrator"
// "github.com/portainer/portainer/api/internal/authorization"
// )
const dummyLogoURL = "example.com"
// const dummyLogoURL = "example.com"
// initTestingDBConn creates a settings service with raw database DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
//insert a obj
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
}
// func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
// //insert a obj
// return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
// }
func setup(store *Store) error {
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
// func setup(store *Store) error {
// dummySettingsObj := map[string]interface{}{
// "LogoURL": dummyLogoURL,
// }
return initTestingSettingsService(store.connection, dummySettingsObj)
}
// return initTestingSettingsService(store.connection, dummySettingsObj)
// }
func TestMigrateSettings(t *testing.T) {
_, store := MustNewTestStore(t, false, true)
// func TestMigrateSettings(t *testing.T) {
// _, store := MustNewTestStore(t, false, true)
err := setup(store)
if err != nil {
t.Errorf("failed to complete testing setups, err: %v", err)
}
// // err := setup(store)
// // if err != nil {
// // t.Errorf("failed to complete testing setups, err: %v", err)
// // }
updatedSettings, err := store.SettingsService.Settings()
// SO -basically, this test _isn't_ testing migration, its testing golang type defaults.
if updatedSettings.LogoURL != dummyLogoURL { // ensure a pre-migrate setting isn't unset
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
// updatedSettings, err := store.SettingsService.Settings()
// // SO -basically, this test _isn't_ testing migration, its testing golang type defaults.
// if updatedSettings.LogoURL != dummyLogoURL { // ensure a pre-migrate setting isn't unset
// t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
// }
if updatedSettings.OAuthSettings.SSO != false { // I recon golang defaulting will make this false
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
// if updatedSettings.OAuthSettings.SSO != false { // I recon golang defaulting will make this false
// t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
// }
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
// if updatedSettings.OAuthSettings.LogoutURI != "" {
// t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
// }
m := migrator.NewMigrator(&migrator.MigratorParameters{
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
})
// m := migrator.NewMigrator(&migrator.MigratorParameters{
// EndpointGroupService: store.EndpointGroupService,
// EndpointService: store.EndpointService,
// EndpointRelationService: store.EndpointRelationService,
// ExtensionService: store.ExtensionService,
// RegistryService: store.RegistryService,
// ResourceControlService: store.ResourceControlService,
// RoleService: store.RoleService,
// ScheduleService: store.ScheduleService,
// SettingsService: store.SettingsService,
// StackService: store.StackService,
// TagService: store.TagService,
// TeamMembershipService: store.TeamMembershipService,
// UserService: store.UserService,
// VersionService: store.VersionService,
// FileService: store.fileService,
// DockerhubService: store.DockerHubService,
// AuthorizationService: authorization.NewService(store),
// })
if err := m.MigrateSettingsToDB30(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
// if err := m.MigrateSettingsToDB30(); err != nil {
// t.Errorf("failed to update settings: %v", err)
// }
if err != nil {
t.Errorf("failed to retrieve the updated settings: %v", err)
}
// if err != nil {
// t.Errorf("failed to retrieve the updated settings: %v", err)
// }
if updatedSettings.LogoURL != dummyLogoURL {
t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
}
// if updatedSettings.LogoURL != dummyLogoURL {
// t.Errorf("unexpected value changes in the updated settings, want LogoURL value: %s, got LogoURL value: %s", dummyLogoURL, updatedSettings.LogoURL)
// }
if updatedSettings.OAuthSettings.SSO != false {
t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
}
// if updatedSettings.OAuthSettings.SSO != false {
// t.Errorf("unexpected default OAuth SSO setting, want: false, got: %t", updatedSettings.OAuthSettings.SSO)
// }
if updatedSettings.OAuthSettings.LogoutURI != "" {
t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
}
}
// if updatedSettings.OAuthSettings.LogoutURI != "" {
// t.Errorf("unexpected default OAuth HideInternalAuth setting, want:, got: %s", updatedSettings.OAuthSettings.LogoutURI)
// }
// }

View file

@ -1,115 +1,115 @@
package datastore
import (
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
)
// 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"
)
// 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",
80: "2.17",
}
// 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",
// 80: "2.17",
// }
func dbVersionToSemanticVersion(dbVersion int) string {
if dbVersion < 18 {
return "1.0.0"
}
// func dbVersionToSemanticVersion(dbVersion int) string {
// if dbVersion < 18 {
// return "1.0.0"
// }
ver, ok := dbVerToSemVerMap[dbVersion]
if ok {
return ver
}
// 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"
}
// // 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"
}
// 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 := ""
// // 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
}
// // 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(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(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
}
// 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
}
// 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)
// // 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
}
// // 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

@ -584,10 +584,10 @@ func (store *Store) Export(filename string) (err error) {
backup.Version = *version
}
backup.Metadata, err = store.connection.BackupMetadata()
if err != nil {
log.Error().Err(err).Msg("exporting Metadata")
}
// backup.Metadata, err = store.connection.BackupMetadata()
// if err != nil {
// log.Error().Err(err).Msg("exporting Metadata")
// }
b, err := json.MarshalIndent(backup, "", " ")
if err != nil {
@ -689,5 +689,5 @@ func (store *Store) Import(filename string) (err error) {
store.Webhook().UpdateWebhook(v.ID, &v)
}
return store.connection.RestoreMetadata(backup.Metadata)
return nil
}