mirror of https://github.com/portainer/portainer
feat(database): add a flag to compact on startup BE-12283 (#1256)
parent
c21c91632f
commit
dcfe2d9809
|
@ -56,6 +56,7 @@ func CLIFlags() *portainer.CLIFlags {
|
||||||
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
|
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
|
||||||
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
|
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
|
||||||
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
|
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
|
||||||
|
CompactDB: kingpin.Flag("compact-db", "Enable database compaction on startup").Envar(portainer.CompactDBEnvVar).Default("false").Bool(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
|
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
|
||||||
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
|
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey, *flags.CompactDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed creating database connection")
|
log.Fatal().Err(err).Msg("failed creating database connection")
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ import (
|
||||||
const (
|
const (
|
||||||
DatabaseFileName = "portainer.db"
|
DatabaseFileName = "portainer.db"
|
||||||
EncryptedDatabaseFileName = "portainer.edb"
|
EncryptedDatabaseFileName = "portainer.edb"
|
||||||
|
|
||||||
|
txMaxSize = 65536
|
||||||
|
compactedSuffix = ".compacted"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -35,6 +38,7 @@ type DbConnection struct {
|
||||||
InitialMmapSize int
|
InitialMmapSize int
|
||||||
EncryptionKey []byte
|
EncryptionKey []byte
|
||||||
isEncrypted bool
|
isEncrypted bool
|
||||||
|
Compact bool
|
||||||
|
|
||||||
*bolt.DB
|
*bolt.DB
|
||||||
}
|
}
|
||||||
|
@ -135,12 +139,7 @@ func (connection *DbConnection) Open() error {
|
||||||
// Now we open the db
|
// Now we open the db
|
||||||
databasePath := connection.GetDatabaseFilePath()
|
databasePath := connection.GetDatabaseFilePath()
|
||||||
|
|
||||||
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
|
db, err := bolt.Open(databasePath, 0600, connection.boltOptions())
|
||||||
Timeout: 1 * time.Second,
|
|
||||||
InitialMmapSize: connection.InitialMmapSize,
|
|
||||||
FreelistType: bolt.FreelistMapType,
|
|
||||||
NoFreelistSync: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -149,6 +148,15 @@ func (connection *DbConnection) Open() error {
|
||||||
db.MaxBatchDelay = connection.MaxBatchDelay
|
db.MaxBatchDelay = connection.MaxBatchDelay
|
||||||
connection.DB = db
|
connection.DB = db
|
||||||
|
|
||||||
|
if connection.Compact {
|
||||||
|
log.Info().Msg("compacting database")
|
||||||
|
if err := connection.compact(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to compact database")
|
||||||
|
} else {
|
||||||
|
log.Info().Msg("database compaction completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,3 +422,42 @@ func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compact attempts to compact the database and replace it iff it succeeds
|
||||||
|
func (connection *DbConnection) compact() error {
|
||||||
|
compactedPath := connection.GetDatabaseFilePath() + compactedSuffix
|
||||||
|
compactedDB, err := bolt.Open(compactedPath, 0o600, connection.boltOptions())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failure to create the compacted database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
compactedDB.MaxBatchSize = connection.MaxBatchSize
|
||||||
|
compactedDB.MaxBatchDelay = connection.MaxBatchDelay
|
||||||
|
|
||||||
|
if err := bolt.Compact(compactedDB, connection.DB, txMaxSize); err != nil {
|
||||||
|
return fmt.Errorf("failure to compact the database: %w",
|
||||||
|
errors.Join(err, compactedDB.Close(), os.Remove(compactedPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(compactedPath, connection.GetDatabaseFilePath()); err != nil {
|
||||||
|
return fmt.Errorf("failure to move the compacted database: %w",
|
||||||
|
errors.Join(err, compactedDB.Close(), os.Remove(compactedPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := connection.Close(); err != nil {
|
||||||
|
log.Warn().Err(err).Msg("failure to close the database after compaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.DB = compactedDB
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (connection *DbConnection) boltOptions() *bolt.Options {
|
||||||
|
return &bolt.Options{
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
InitialMmapSize: connection.InitialMmapSize,
|
||||||
|
FreelistType: bolt.FreelistMapType,
|
||||||
|
NoFreelistSync: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_NeedsEncryptionMigration(t *testing.T) {
|
func Test_NeedsEncryptionMigration(t *testing.T) {
|
||||||
|
@ -119,3 +121,57 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDBCompaction(t *testing.T) {
|
||||||
|
db := &DbConnection{
|
||||||
|
Path: t.TempDir(),
|
||||||
|
Compact: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists([]byte("testbucket"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Put([]byte("key"), []byte("value"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Reopen the DB to trigger compaction
|
||||||
|
err = db.Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the data is still there
|
||||||
|
err = db.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("testbucket"))
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
val := b.Get([]byte("key"))
|
||||||
|
require.Equal(t, []byte("value"), val)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Failures
|
||||||
|
|
||||||
|
err = os.Mkdir(db.GetDatabaseFilePath()+compactedSuffix, 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
|
@ -8,11 +8,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDatabase should use config options to return a connection to the requested database
|
// 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) {
|
func NewDatabase(storeType, storePath string, encryptionKey []byte, compact bool) (connection portainer.Connection, err error) {
|
||||||
if storeType == "boltdb" {
|
if storeType == "boltdb" {
|
||||||
return &boltdb.DbConnection{
|
return &boltdb.DbConnection{
|
||||||
Path: storePath,
|
Path: storePath,
|
||||||
EncryptionKey: encryptionKey,
|
EncryptionKey: encryptionKey,
|
||||||
|
Compact: compact,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error)
|
||||||
secretKey = nil
|
secretKey = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
connection, err := database.NewDatabase("boltdb", storePath, secretKey)
|
connection, err := database.NewDatabase("boltdb", storePath, secretKey, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ type datastoreOption = func(d *testDatastore)
|
||||||
// NewDatastore creates new instance of testDatastore.
|
// NewDatastore creates new instance of testDatastore.
|
||||||
// Will apply options before returning, opts will be applied from left to right.
|
// Will apply options before returning, opts will be applied from left to right.
|
||||||
func NewDatastore(options ...datastoreOption) *testDatastore {
|
func NewDatastore(options ...datastoreOption) *testDatastore {
|
||||||
conn, _ := database.NewDatabase("boltdb", "", nil)
|
conn, _ := database.NewDatabase("boltdb", "", nil, false)
|
||||||
d := testDatastore{connection: conn}
|
d := testDatastore{connection: conn}
|
||||||
|
|
||||||
for _, o := range options {
|
for _, o := range options {
|
||||||
|
|
|
@ -112,6 +112,7 @@ type (
|
||||||
AdminPasswordFile *string
|
AdminPasswordFile *string
|
||||||
Assets *string
|
Assets *string
|
||||||
CSP *bool
|
CSP *bool
|
||||||
|
CompactDB *bool
|
||||||
Data *string
|
Data *string
|
||||||
FeatureFlags *[]string
|
FeatureFlags *[]string
|
||||||
EnableEdgeComputeFeatures *bool
|
EnableEdgeComputeFeatures *bool
|
||||||
|
@ -1846,6 +1847,8 @@ const (
|
||||||
TrustedOriginsEnvVar = "TRUSTED_ORIGINS"
|
TrustedOriginsEnvVar = "TRUSTED_ORIGINS"
|
||||||
// CSPEnvVar is the environment variable used to enable/disable the Content Security Policy
|
// CSPEnvVar is the environment variable used to enable/disable the Content Security Policy
|
||||||
CSPEnvVar = "CSP"
|
CSPEnvVar = "CSP"
|
||||||
|
// CompactDBEnvVar is the environment variable used to enable/disable the startup compaction of the database
|
||||||
|
CompactDBEnvVar = "COMPACT_DB"
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of supported features
|
// List of supported features
|
||||||
|
|
Loading…
Reference in New Issue