1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

chore(store) EE-1981: Refactor/store/error checking, and other refactoring (#6173)

* use the Store interface IsErrObjectNotFound() to avoid revealing internal errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* what happens when you extract the datastore interfaces into their own package

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Start renaming Storage methods

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the boltdb specific code from the Portainer storage code (example, the others need the same)

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extract bolt.Tx from datastore code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* minimise imports by putting moving the struct definition into the file that needs the Service imports

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extraction of boltdb.Tx

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the use of bucket.SetSequence

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* almost done - just endpoint.Synchonise :/

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* so, endpoint.Synchonize looks hard, but i can't find where we use it, so 'delete first refactoring'

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix test compile errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* test compile fixes after rebase

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix a mis-remembering I had wrt deserialisation - last time i used AnyData - jsoniter's bindTo looks interesting for the same reason

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* set us up to make the connection an interface

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make the db connection a datastore interface, and separate out our datastore services from the bolt ones

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* rename methods to something less oltdb internals specific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* these errors are not boltdb secific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* start using the db-backend factory method too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* export boltdb raw in case we can't export from the service layer

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add a raw export from boltdb to yaml for broken db's, and an export services to yaml in backup

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add the version info by hand for now

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* actually, the export from services can be fully typed - its the import that needs to do more work

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* redo raw export, and make import capable of using it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add DockerHub

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* migration from anything older than v1.21.0 has been broken for quite a while, deleting the un-tested code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix go test ./... again

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* my goland wasn't setup to gofmt

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the two extremely dubious migration tests down into store, so they can use the test store code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* the migrator is now free of boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* reverse goland overzealous replcement of internal with boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more undo over-zealous goland internal->boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* yay, now bolt is only mentioned inside the api/database/ dir

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* and this might be the last of the boltdb references?

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add todo

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the store code into a separate module too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* don't need the fileService in boltdb anymore

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use IsErrObjectNotFound()

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use a string to select what database backend we use

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make isNew store an ephemeral bool that doesn't stay true after we've initialised it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the import.json wip to a separate file so its more obvious - we'll be using it for testing, emergency fixups, and in the next part of the store work, when we improve migrations and data model lifecycles

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* undo vscode formatting html

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix app templates symbol (#6221)

* feat(webhook) EE-2125 send registry auth haeder when update swarms service via webhook (#6220)

* feat(webhook) EE-2125 add some helpers to registry utils

* feat(webhook) EE-2125 persist registryID when creating a webhook

* feat(webhook) EE-2125 send registry auth header when executing a webhook

* feat(webhook) EE-2125 send registryID to backend when creating a service with webhook

* feat(webhook) EE-2125 use the initial registry ID to create webhook on editing service screen

* feat(webhook) EE-2125 update webhook when update registry

* feat(webhook) EE-2125 add endpoint of update webhook

* feat(webhook) EE-2125 code cleanup

* feat(webhook) EE-2125 fix a typo

* feat(webhook) EE-2125 fix circle import issue with unit test

Co-authored-by: Simon Meng <simon.meng@portainer.io>

* fix(kubeconfig): show kubeconfig download button for non admin users [EE-2123] (#6204)

Co-authored-by: Simon Meng <simon.meng@portainer.io>

* fix data-cy for k8s cluster menu (#6226)

LGTM

* feat(stack): make stack created from app template editable EE-1941 (#6104)

feat(stack): make stack from app template editable

* fix(container):disable Duplicate/Edit button when the container is portainer (#6223)

* fix/ee-1909/show-pull-image-error (#6195)

Co-authored-by: sunportainer <ericsun@SG1.local>

* feat(cy): add data-cy to helm install button (#6241)

* feat(cy): add data-cy to add registry button (#6242)

* refactor(app): convert root folder files to es6 (#4159)

* refactor(app): duplicate constants as es6 exports (#4158)

* fix(docker): provide workaround to save network name variable  (#6080)

* fix/EE-1862/unable-to-stop-or-remove-stack workaround for var without default value in yaml file

* fix/EE-1862/unable-to-stop-or-remove-stack check yaml file

* fixed func and var names

* wrapper error and used bool for stringset

* UT case for createNetworkEnvFile

* UT case for %s=%s

* powerful StringSet

* wrapper error for extract network name

* wrapper all the return err

* store more env

* put to env file

* make default value None

* feat: gzip static resources (#6258)

* fix(ssl)//handle --sslcert and --sslkey ee-2106 (#6203)

* fix/ee-2106/handle-sslcert-sslkey

Co-authored-by: sunportainer <ericsun@SG1.local>

* fix(server):support disable https only ee-2068 (#6232)

* fix/ee-2068/disable-forcely-https

* feat(store): implement store tests EE-2112 (#6224)

* add store tests

* add some more tests

* Update missing helm user repo methods

* remove redundant comments

* add webhook export

* update webhooks

* use the Store interface IsErrObjectNotFound() to avoid revealing internal errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* what happens when you extract the datastore interfaces into their own package

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Start renaming Storage methods

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the boltdb specific code from the Portainer storage code (example, the others need the same)

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extract bolt.Tx from datastore code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* minimise imports by putting moving the struct definition into the file that needs the Service imports

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more extraction of boltdb.Tx

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the use of bucket.SetSequence

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* almost done - just endpoint.Synchonise :/

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* so, endpoint.Synchonize looks hard, but i can't find where we use it, so 'delete first refactoring'

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix test compile errors

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* test compile fixes after rebase

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix a mis-remembering I had wrt deserialisation - last time i used AnyData - jsoniter's bindTo looks interesting for the same reason

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* set us up to make the connection an interface

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make the db connection a datastore interface, and separate out our datastore services from the bolt ones

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* rename methods to something less oltdb internals specific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* these errors are not boltdb secific

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* start using the db-backend factory method too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* export boltdb raw in case we can't export from the service layer

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add a raw export from boltdb to yaml for broken db's, and an export services to yaml in backup

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add the version info by hand for now

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* actually, the export from services can be fully typed - its the import that needs to do more work

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* redo raw export, and make import capable of using it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add DockerHub

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* migration from anything older than v1.21.0 has been broken for quite a while, deleting the un-tested code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix go test ./... again

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* my goland wasn't setup to gofmt

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the two extremely dubious migration tests down into store, so they can use the test store code

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* the migrator is now free of boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* reverse goland overzealous replcement of internal with boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* more undo over-zealous goland internal->boltdb

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* yay, now bolt is only mentioned inside the api/database/ dir

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* and this might be the last of the boltdb references?

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* add todo

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* extract the store code into a separate module too

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* don't need the fileService in boltdb anymore

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use IsErrObjectNotFound()

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* use a string to select what database backend we use

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* make isNew store an ephemeral bool that doesn't stay true after we've initialised it

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* move the import.json wip to a separate file so its more obvious - we'll be using it for testing, emergency fixups, and in the next part of the store work, when we improve migrations and data model lifecycles

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* undo vscode formatting html

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* Update missing helm user repo methods

* feat(store): implement store tests EE-2112 (#6224)

* add store tests

* add some more tests

* remove redundant comments

* add webhook export

* update webhooks

* fix build issues after rebasing

* move migratorparams

* remove unneeded integer type conversions

* disable the db import/export for now

Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com>
Co-authored-by: cong meng <mcpacino@gmail.com>
Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Marcelo Rydel <marcelorydel26@gmail.com>
Co-authored-by: Hao Zhang <hao.zhang@portainer.io>
Co-authored-by: sunportainer <93502624+sunportainer@users.noreply.github.com>
Co-authored-by: sunportainer <ericsun@SG1.local>
Co-authored-by: wheresolivia <78844659+wheresolivia@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: Chao Geng <93526589+chaogeng77977@users.noreply.github.com>
Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: Matt Hook <hookenz@gmail.com>
This commit is contained in:
Sven Dowideit 2021-12-15 12:26:09 +10:00 committed by GitHub
parent b02bf0c9d7
commit f99329eb7e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
299 changed files with 5246 additions and 5092 deletions

155
api/datastore/backup.go Normal file
View file

@ -0,0 +1,155 @@
package datastore
import (
"fmt"
"os"
"path"
"time"
plog "github.com/portainer/portainer/api/datastore/log"
)
var backupDefaults = struct {
backupDir string
commonDir string
}{
"backups",
"common",
}
var backupLog = plog.NewScopedLog("database, backup")
//
// 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 {
backupLog.Error("Error while creating common backup folder", err)
}
}
}
func (store *Store) databasePath() string {
return path.Join(store.connection.GetStorePath(), store.connection.GetDatabaseFilename())
}
func (store *Store) commonBackupDir() string {
return path.Join(store.connection.GetStorePath(), backupDefaults.backupDir, backupDefaults.commonDir)
}
func (store *Store) copyDBFile(from string, to string) error {
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
err := store.fileService.Copy(from, to, true)
if err != nil {
backupLog.Error("Failed", err)
}
return err
}
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Version int // I can't find this used for anything other than a filename
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,
}
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
}
if options.Version == 0 {
version, err := store.version()
if err != nil {
version = 0
}
options.Version = version
}
if options.BackupDir == "" {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFilename(), fmt.Sprintf("%03d", 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) {
backupLog.Info("creating db backup")
store.createBackupFolders()
options = store.setupOptions(options)
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
}
// 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) {
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
return err
}
err = store.Close()
if err != nil {
backupLog.Error("Error while closing store before restore", err)
return err
}
backupLog.Info("Restoring db backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
return err
}
_, err = store.Open()
return err
}
// RemoveWithOptions removes backup database based on supplied options
func (store *Store) removeWithOptions(options *BackupOptions) error {
backupLog.Info("Removing db backup")
options = store.setupOptions(options)
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
return err
}
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
err = os.Remove(options.BackupPath)
if err != nil {
backupLog.Error("Failed", err)
return err
}
return nil
}

View file

@ -0,0 +1,108 @@
package datastore
import (
"fmt"
"os"
"path"
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
connection := store.GetConnection()
backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir)
if isFileExist(backupPath) {
t.Error("Expect backups folder to not exist")
}
store.createBackupFolders()
if !isFileExist(backupPath) {
t.Error("Expect backups folder to exist")
}
}
func TestStoreCreation(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
defer teardown()
if store == nil {
t.Error("Expect to create a store")
}
if store.CheckCurrentEdition() != nil {
t.Error("Expect to get CE Edition")
}
}
func TestBackup(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
connection := store.GetConnection()
defer teardown()
t.Run("Backup should create default db backup", func(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
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, teardown := MustNewTestStore(true)
defer teardown()
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()
err = store.removeWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
}
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",
}
err := store.removeWithOptions(options)
if err == nil {
t.Error("RemoveWithOptions should fail for non-existent file")
}
})
}

View file

@ -0,0 +1,80 @@
package datastore
import (
"io"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
)
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if store.IsErrObjectNotFound(err) {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
return &Store{
fileService: fileService,
connection: connection,
}
}
// Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) {
newStore = true
err = store.connection.Open()
if err != nil {
return newStore, err
}
err = store.initServices()
if err != nil {
return newStore, err
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
if _, err := store.VersionService.DBVersion(); err == nil {
newStore = false
}
return newStore, nil
}
func (store *Store) Close() error {
return store.connection.Close()
}
// 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.PortainerCE {
return errors.ErrWrongDBEdition
}
return nil
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == errors.ErrObjectNotFound
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
}

View file

@ -0,0 +1,418 @@
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, teardown := MustNewTestStore(true)
defer teardown()
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{},
Extensions: []portainer.EndpointExtension{},
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")
}

104
api/datastore/init.go Normal file
View file

@ -0,0 +1,104 @@
package datastore
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
)
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
}
// TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings()
if store.IsErrObjectNotFound(err) {
defaultSettings := &portainer.Settings{
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
KubectlShellImage: portainer.DefaultKubectlShellImage,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return err
} else if err == nil {
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
store.Settings().UpdateSettings(settings)
}
}
_, err = store.SSLSettings().Settings()
if err != nil {
if !store.IsErrObjectNotFound(err) {
return err
}
defaultSSLSettings := &portainer.SSLSettings{
HTTPEnabled: true,
}
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
if err != nil {
return err
}
}
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
}
if len(groups) == 0 {
unassignedGroup := &portainer.EndpointGroup{
Name: "Unassigned",
Description: "Unassigned environments",
Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
TagIDs: []portainer.TagID{},
}
err = store.EndpointGroupService.Create(unassignedGroup)
if err != nil {
return err
}
}
return nil
}

41
api/datastore/log/log.go Normal file
View file

@ -0,0 +1,41 @@
package log
import (
"fmt"
"log"
)
const (
INFO = "INFO"
ERROR = "ERROR"
DEBUG = "DEBUG"
FATAL = "FATAL"
)
type ScopedLog struct {
scope string
}
func NewScopedLog(scope string) *ScopedLog {
return &ScopedLog{scope: scope}
}
func (slog *ScopedLog) print(kind string, message string) {
log.Printf("[%s] [%s] %s", kind, slog.scope, message)
}
func (slog *ScopedLog) Debug(message string) {
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Info(message string) {
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}
func (slog *ScopedLog) NotImplemented(method string) {
log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
}

View file

@ -0,0 +1,134 @@
package datastore
import (
"fmt"
"runtime/debug"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/dataservices/errors"
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
var migrateLog = plog.NewScopedLog("database, migrate")
func (store *Store) MigrateData() error {
version, err := store.version()
if err != nil {
return err
}
migratorParams := &migrator.MigratorParameters{
DatabaseVersion: 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,
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),
}
return store.connectionMigrateData(migratorParams)
}
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
defer func() {
if e := recover(); e != nil {
store.Rollback(true)
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
}
}()
// !Important: we must use a named return value in the function definition and not a local
// !variable referenced from the closure or else the return value will be incorrectly set
return migrator.Migrate()
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
// if force is true, then migrate regardless.
func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParameters) error {
migrator := migrator.NewMigrator(migratorParams)
// backup db file before upgrading DB to support rollback
isUpdating, err := migratorParams.VersionService.IsUpdating()
if err != nil && err != errors.ErrObjectNotFound {
return err
}
if !isUpdating && migrator.Version() != portainer.DBVersion {
err = store.backupVersion(migrator)
if err != nil {
return werrors.Wrapf(err, "failed to backup database")
}
}
if migrator.Version() < portainer.DBVersion {
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
err = store.FailSafeMigrate(migrator)
if err != nil {
migrateLog.Error("An error occurred during database migration", err)
return err
}
}
return nil
}
// backupVersion will backup the database or panic if any errors occur
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
migrateLog.Info("Backing up database prior to version upgrade...")
options := getBackupRestoreOptions(store.commonBackupDir())
_, err := store.backupWithOptions(options)
if err != nil {
migrateLog.Error("An error occurred during database backup", err)
removalErr := store.removeWithOptions(options)
if removalErr != nil {
migrateLog.Error("An error occurred during store removal prior to backup", err)
}
return err
}
return nil
}
// 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
}
}
options := getBackupRestoreOptions(store.commonBackupDir())
err := store.restoreWithOptions(options)
if err != nil {
return err
}
return store.connection.Close()
}

View file

@ -0,0 +1,187 @@
package datastore
import (
"fmt"
"log"
"path/filepath"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
)
// testVersion is a helper which tests current store version against wanted version
func testVersion(store *Store, versionWant int, t *testing.T) {
v, err := store.VersionService.DBVersion()
if err != nil {
t.Errorf("Expect store version to be %d but was %d with error: %s", versionWant, v, err)
}
if v != versionWant {
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
}
}
func TestMigrateData(t *testing.T) {
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
newStore, store, teardown := MustNewTestStore(false)
defer teardown()
if !newStore {
t.Error("Expect a new DB")
}
// not called for new stores
//store.MigrateData()
testVersion(store, portainer.DBVersion, t)
store.Close()
newStore, _ = store.Open()
if newStore {
t.Error("Expect store to NOT be new DB")
}
})
tests := []struct {
version int
expectedVersion int
}{
{version: 17, expectedVersion: portainer.DBVersion},
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(true)
defer teardown()
// Setup data
store.VersionService.StoreDBVersion(tc.version)
// Required roles by migrations 22.2
store.RoleService.Create(&portainer.Role{ID: 1})
store.RoleService.Create(&portainer.Role{ID: 2})
store.RoleService.Create(&portainer.Role{ID: 3})
store.RoleService.Create(&portainer.Role{ID: 4})
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
store.MigrateData()
testVersion(store, tc.expectedVersion, t)
})
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
store.Rollback(true)
store.Open()
testVersion(store, tc.version, t)
})
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
version := 17
store.VersionService.StoreDBVersion(version)
store.MigrateData()
testVersion(store, version, t)
})
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(0)
store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if !isFileExist(options.BackupPath) {
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
}
})
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreIsUpdating(true)
store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
wantDir := store.commonBackupDir()
if !strings.HasSuffix(options.BackupDir, wantDir) {
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
}
wantFilename := "portainer.db.bak"
if options.BackupFileName != wantFilename {
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
}
}
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := 21
_, store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(version)
_, err := store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
if err != nil {
log.Fatal(err)
}
// Change the current edition
err = store.VersionService.StoreDBVersion(version + 10)
if err != nil {
log.Fatal(err)
}
err = store.Rollback(true)
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
store.Open()
testVersion(store, version, t)
})
}
// isFileExist is helper function to check for file existence
func isFileExist(path string) bool {
matches, err := filepath.Glob(path)
if err != nil {
return false
}
return len(matches) > 0
}

View file

@ -0,0 +1,91 @@
package datastore
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
)
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
if err := dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj); err != nil {
return err
}
return nil
}
func setup(store *Store) error {
var err error
dummySettingsObj := map[string]interface{}{
"LogoURL": dummyLogoURL,
}
err = initTestingSettingsService(store.connection, dummySettingsObj)
if err != nil {
return err
}
return nil
}
func TestMigrateSettings(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
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)
}
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)
}
m := migrator.NewMigrator(&migrator.MigratorParameters{
DatabaseVersion: 29,
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 != 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.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)
}
}

View file

@ -0,0 +1,53 @@
package datastore
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore/migrator"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
)
func TestMigrateStackEntryPoint(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
defer teardown()
stackService := store.Stack()
stacks := []*portainer.Stack{
{
ID: 1,
EntryPoint: "dir/sub/compose.yml",
},
{
ID: 2,
EntryPoint: "dir/sub/compose.yml",
GitConfig: &gittypes.RepoConfig{},
},
}
for _, s := range stacks {
err := stackService.Create(s)
assert.NoError(t, err, "failed to create stack")
}
s, err := stackService.Stack(1)
assert.NoError(t, err)
assert.Nil(t, s.GitConfig, "first stack should not have git config")
s, err = stackService.Stack(2)
assert.NoError(t, err)
assert.Equal(t, "", s.GitConfig.ConfigFilePath, "not migrated yet migrated")
err = migrator.MigrateStackEntryPoint(stackService)
assert.NoError(t, err, "failed to migrate entry point to Git ConfigFilePath")
s, err = stackService.Stack(1)
assert.NoError(t, err)
assert.Nil(t, s.GitConfig, "first stack should not have git config")
s, err = stackService.Stack(2)
assert.NoError(t, err)
assert.Equal(t, "dir/sub/compose.yml", s.GitConfig.ConfigFilePath, "second stack should have config file path migrated")
}

View file

@ -0,0 +1,194 @@
package migrator
import (
"fmt"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
func migrationError(err error, context string) error {
return werrors.Wrap(err, "failed in "+context)
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// set DB to updating status
err := m.versionService.StoreIsUpdating(true)
if err != nil {
return migrationError(err, "StoreIsUpdating")
}
if m.currentDBVersion < 17 {
return migrationError(err, "migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return migrationError(err, "updateUsersToDBVersion18")
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion18")
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion19")
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return migrationError(err, "updateUsersToDBVersion20")
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion20")
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return migrationError(err, "updateSchedulesToDBVersion20")
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion22")
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return migrationError(err, "updateUsersAndRolesToDBVersion22")
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
migrateLog.Info("Migrating to DB 23")
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
migrateLog.Info("Migrating to DB 24")
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
migrateLog.Info("Migrating to DB 25")
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
}
err = m.updateStacksToDB24()
if err != nil {
return migrationError(err, "updateStacksToDB24")
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
migrateLog.Info("Migrating to DB 26")
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
migrateLog.Info("Migrating to DB 27")
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
migrateLog.Info("Migrating to DB 30")
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return migrationError(err, "migrateDBVersionToDB32")
}
}
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
migrateLog.Info("Migrating to DB 33")
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
migrateLog.Info("Migrating to DB 34")
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
migrateLog.Info("Migrating to DB 35")
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
}
}
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
// reset DB updating status
return m.versionService.StoreIsUpdating(false)
}

View file

@ -0,0 +1,125 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateUsersToDBVersion18() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = map[portainer.Authorization]bool{
portainer.OperationPortainerDockerHubInspect: true,
portainer.OperationPortainerEndpointGroupList: true,
portainer.OperationPortainerEndpointList: true,
portainer.OperationPortainerEndpointInspect: true,
portainer.OperationPortainerEndpointExtensionAdd: true,
portainer.OperationPortainerEndpointExtensionRemove: true,
portainer.OperationPortainerExtensionList: true,
portainer.OperationPortainerMOTD: true,
portainer.OperationPortainerRegistryList: true,
portainer.OperationPortainerRegistryInspect: true,
portainer.OperationPortainerTeamList: true,
portainer.OperationPortainerTemplateList: true,
portainer.OperationPortainerTemplateInspect: true,
portainer.OperationPortainerUserList: true,
portainer.OperationPortainerUserMemberships: true,
}
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.UserAccessPolicies = make(portainer.UserAccessPolicies)
for _, userID := range endpoint.AuthorizedUsers {
endpoint.UserAccessPolicies[userID] = portainer.AccessPolicy{
RoleID: 4,
}
}
endpoint.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
for _, teamID := range endpoint.AuthorizedTeams {
endpoint.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
RoleID: 4,
}
}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range legacyEndpointGroups {
endpointGroup.UserAccessPolicies = make(portainer.UserAccessPolicies)
for _, userID := range endpointGroup.AuthorizedUsers {
endpointGroup.UserAccessPolicies[userID] = portainer.AccessPolicy{
RoleID: 4,
}
}
endpointGroup.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
for _, teamID := range endpointGroup.AuthorizedTeams {
endpointGroup.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
RoleID: 4,
}
}
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err
}
for _, registry := range legacyRegistries {
registry.UserAccessPolicies = make(portainer.UserAccessPolicies)
for _, userID := range registry.AuthorizedUsers {
registry.UserAccessPolicies[userID] = portainer.AccessPolicy{}
}
registry.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
for _, teamID := range registry.AuthorizedTeams {
registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{}
}
err = m.registryService.UpdateRegistry(registry.ID, &registry)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,16 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion19() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.EdgeAgentCheckinInterval == 0 {
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
}
return m.settingsService.UpdateSettings(legacySettings)
}

View file

@ -0,0 +1,57 @@
package migrator
import (
"strings"
)
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
return m.authorizationService.UpdateUsersAuthorizations()
}
func (m *Migrator) updateSettingsToDBVersion20() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowVolumeBrowserForRegularUsers = false
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err
}
for _, schedule := range legacySchedules {
if schedule.JobType == scheduleScriptExecutionJobType {
if schedule.CronExpression == "0 0 * * *" {
schedule.CronExpression = "0 * * * *"
} else if schedule.CronExpression == "0 0 0/2 * *" {
schedule.CronExpression = "0 */2 * * *"
} else if schedule.CronExpression == "0 0 0 * *" {
schedule.CronExpression = "0 0 * * *"
} else {
revisedCronExpression := strings.Split(schedule.CronExpression, " ")
if len(revisedCronExpression) == 5 {
continue
}
revisedCronExpression = revisedCronExpression[1:]
schedule.CronExpression = strings.Join(revisedCronExpression, " ")
}
err := m.scheduleService.UpdateSchedule(schedule.ID, &schedule)
if err != nil {
return err
}
}
}
return nil
}

View file

@ -0,0 +1,85 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resourceControl := range legacyResourceControls {
resourceControl.AdministratorsOnly = false
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
for _, user := range legacyUsers {
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
if err != nil {
return err
}
endpointAdministratorRole.Priority = 1
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
if err != nil {
return err
}
helpDeskRole.Priority = 2
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
if err != nil {
return err
}
standardUserRole.Priority = 3
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
if err != nil {
return err
}
readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
if err != nil {
return err
}
return m.authorizationService.UpdateUsersAuthorizations()
}

View file

@ -0,0 +1,94 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
migrateLog.Info("Updating tags")
tags, err := m.tagService.Tags()
if err != nil {
return err
}
for _, tag := range tags {
tag.EndpointGroups = make(map[portainer.EndpointGroupID]bool)
tag.Endpoints = make(map[portainer.EndpointID]bool)
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
migrateLog.Info("Updating endpoints and endpoint groups")
tags, err := m.tagService.Tags()
if err != nil {
return err
}
tagsNameMap := make(map[string]portainer.Tag)
for _, tag := range tags {
tagsNameMap[tag.Name] = tag
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpointTags := make([]portainer.TagID, 0)
for _, tagName := range endpoint.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointTags = append(endpointTags, tag.ID)
tag.Endpoints[endpoint.ID] = true
}
}
endpoint.TagIDs = endpointTags
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = m.endpointRelationService.Create(relation)
if err != nil {
return err
}
}
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
endpointGroupTags := make([]portainer.TagID, 0)
for _, tagName := range endpointGroup.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointGroupTags = append(endpointGroupTags, tag.ID)
tag.EndpointGroups[endpointGroup.ID] = true
}
}
endpointGroup.TagIDs = endpointGroupTags
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
for _, tag := range tagsNameMap {
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,37 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDB24() error {
migrateLog.Info("Updating Settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.AllowHostNamespaceForRegularUsers = true
legacySettings.AllowDeviceMappingForRegularUsers = true
legacySettings.AllowStackManagementForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}
func (m *Migrator) updateStacksToDB24() error {
migrateLog.Info("Updating stacks")
stacks, err := m.stackService.Stacks()
if err != nil {
return err
}
for idx := range stacks {
stack := &stacks[idx]
stack.Status = portainer.StackStatusActive
err := m.stackService.UpdateStack(stack.ID, stack)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,25 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDB25() error {
migrateLog.Info("Updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
if legacySettings.TemplatesURL == "" {
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
}
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
return m.settingsService.UpdateSettings(legacySettings)
}

View file

@ -0,0 +1,52 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateEndpointSettingsToDB25() error {
migrateLog.Info("Updating endpoint settings")
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for i := range endpoints {
endpoint := endpoints[i]
securitySettings := portainer.EndpointSecuritySettings{}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.DockerEnvironment {
securitySettings = portainer.EndpointSecuritySettings{
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
}
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
}
}
endpoint.SecuritySettings = securitySettings
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,41 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/internal/stackutils"
)
func (m *Migrator) updateStackResourceControlToDB27() error {
migrateLog.Info("Updating stack resource controls")
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}
for _, resource := range resourceControls {
if resource.Type != portainer.StackResourceControl {
continue
}
stackName := resource.ResourceID
stack, err := m.stackService.StackByName(stackName)
if err != nil {
if err == errors.ErrObjectNotFound {
continue
}
return err
}
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,24 @@
package migrator
func (m *Migrator) migrateDBVersionToDB30() error {
migrateLog.Info("Updating legacy settings")
if err := m.MigrateSettingsToDB30(); err != nil {
return err
}
return nil
}
// so setting to false and "", is what would happen without this code
// I'm going to bet there's zero point to changing the value inthe DB
// Public for testing
func (m *Migrator) MigrateSettingsToDB30() error {
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err
}
legacySettings.OAuthSettings.SSO = false
legacySettings.OAuthSettings.LogoutURI = ""
return m.settingsService.UpdateSettings(legacySettings)
}

View file

@ -0,0 +1,282 @@
package migrator
import (
"fmt"
"github.com/portainer/portainer/api/dataservices/errors"
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)
func (m *Migrator) migrateDBVersionToDB32() error {
migrateLog.Info("Updating registries")
err := m.updateRegistriesToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating dockerhub")
err = m.updateDockerhubToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating resource controls")
if err := m.updateVolumeResourceControlToDB32(); err != nil {
return err
}
migrateLog.Info("Updating kubeconfig expiry")
if err := m.kubeconfigExpiryToDB32(); err != nil {
return err
}
migrateLog.Info("Setting default helm repository url")
if err := m.helmRepositoryURLToDB32(); err != nil {
return err
}
return nil
}
func (m *Migrator) updateRegistriesToDB32() error {
registries, err := m.registryService.Registries()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, registry := range registries {
registry.RegistryAccesses = portainer.RegistryAccesses{}
for _, endpoint := range endpoints {
filteredUserAccessPolicies := portainer.UserAccessPolicies{}
for userId, registryPolicy := range registry.UserAccessPolicies {
if _, found := endpoint.UserAccessPolicies[userId]; found {
filteredUserAccessPolicies[userId] = registryPolicy
}
}
filteredTeamAccessPolicies := portainer.TeamAccessPolicies{}
for teamId, registryPolicy := range registry.TeamAccessPolicies {
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
filteredTeamAccessPolicies[teamId] = registryPolicy
}
}
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
UserAccessPolicies: filteredUserAccessPolicies,
TeamAccessPolicies: filteredTeamAccessPolicies,
Namespaces: []string{},
}
}
m.registryService.UpdateRegistry(registry.ID, &registry)
}
return nil
}
func (m *Migrator) updateDockerhubToDB32() error {
dockerhub, err := m.dockerhubService.DockerHub()
if err == errors.ErrObjectNotFound {
return nil
} else if err != nil {
return err
}
if !dockerhub.Authentication {
return nil
}
registry := &portainer.Registry{
Type: portainer.DockerHubRegistry,
Name: "Dockerhub (authenticated - migrated)",
URL: "docker.io",
Authentication: true,
Username: dockerhub.Username,
Password: dockerhub.Password,
RegistryAccesses: portainer.RegistryAccesses{},
}
// The following code will make this function idempotent.
// i.e. if run again, it will not change the data. It will ensure that
// we only have one migrated registry entry. Duplicates will be removed
// if they exist and which has been happening due to earlier migration bugs
migrated := false
registries, _ := m.registryService.Registries()
for _, r := range registries {
if r.Type == registry.Type &&
r.Name == registry.Name &&
r.URL == registry.URL &&
r.Authentication == registry.Authentication {
if !migrated {
// keep this one entry
migrated = true
} else {
// delete subsequent duplicates
m.registryService.DeleteRegistry(portainer.RegistryID(r.ID))
}
}
}
if migrated {
return nil
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpoint.Type != portainer.KubernetesLocalEnvironment &&
endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
userAccessPolicies := portainer.UserAccessPolicies{}
for userId := range endpoint.UserAccessPolicies {
if _, found := endpoint.UserAccessPolicies[userId]; found {
userAccessPolicies[userId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
teamAccessPolicies := portainer.TeamAccessPolicies{}
for teamId := range endpoint.TeamAccessPolicies {
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
teamAccessPolicies[teamId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
registry.RegistryAccesses[endpoint.ID] = portainer.RegistryAccessPolicies{
UserAccessPolicies: userAccessPolicies,
TeamAccessPolicies: teamAccessPolicies,
Namespaces: []string{},
}
}
}
return m.registryService.Create(registry)
}
func (m *Migrator) updateVolumeResourceControlToDB32() error {
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return fmt.Errorf("failed fetching environments: %w", err)
}
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return fmt.Errorf("failed fetching resource controls: %w", err)
}
toUpdate := map[portainer.ResourceControlID]string{}
volumeResourceControls := map[string]*portainer.ResourceControl{}
for i := range resourceControls {
resourceControl := resourceControls[i]
if resourceControl.Type == portainer.VolumeResourceControl {
volumeResourceControls[resourceControl.ResourceID] = &resourceControl
}
}
for _, endpoint := range endpoints {
if !endpointutils.IsDockerEndpoint(&endpoint) {
continue
}
totalSnapshots := len(endpoint.Snapshots)
if totalSnapshots == 0 {
log.Println("[DEBUG] [volume migration] [message: no snapshot found]")
continue
}
snapshot := endpoint.Snapshots[totalSnapshots-1]
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
if err != nil {
log.Printf("[WARN] [database,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
continue
}
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
if volumesData["Volumes"] == nil {
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
continue
}
findResourcesToUpdateForDB32(endpointDockerID, volumesData, toUpdate, volumeResourceControls)
}
}
for _, resourceControl := range volumeResourceControls {
if newResourceID, ok := toUpdate[resourceControl.ID]; ok {
resourceControl.ResourceID = newResourceID
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
if err != nil {
return fmt.Errorf("failed updating resource control %d: %w", resourceControl.ID, err)
}
} else {
err := m.resourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
}
log.Printf("[DEBUG] [volume migration] [message: legacy resource control(%s) has been deleted]", resourceControl.ResourceID)
}
}
return nil
}
func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interface{}, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
volumes := volumesData["Volumes"].([]interface{})
for _, volumeMeta := range volumes {
volume := volumeMeta.(map[string]interface{})
volumeName, nameExist := volume["Name"].(string)
if !nameExist {
continue
}
createTime, createTimeExist := volume["CreatedAt"].(string)
if !createTimeExist {
continue
}
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
resourceControl, ok := volumeResourceControls[oldResourceID]
if ok {
toUpdate[resourceControl.ID] = fmt.Sprintf("%s_%s", volumeName, dockerID)
}
}
}
func (m *Migrator) kubeconfigExpiryToDB32() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
return m.settingsService.UpdateSettings(settings)
}
func (m *Migrator) helmRepositoryURLToDB32() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
settings.HelmRepositoryURL = portainer.DefaultHelmRepositoryURL
return m.settingsService.UpdateSettings(settings)
}

View file

@ -0,0 +1,22 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB33() error {
if err := m.migrateSettingsToDB33(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateSettingsToDB33() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
migrateLog.Info("Setting default kubectl shell image")
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View file

@ -0,0 +1,32 @@
package migrator
import "github.com/portainer/portainer/api/dataservices"
func (m *Migrator) migrateDBVersionToDB34() error {
migrateLog.Info("Migrating stacks")
err := MigrateStackEntryPoint(m.stackService)
if err != nil {
return err
}
return nil
}
// MigrateStackEntryPoint exported for testing (blah.)
func MigrateStackEntryPoint(stackService dataservices.StackService) error {
stacks, err := stackService.Stacks()
if err != nil {
return err
}
for i := range stacks {
stack := &stacks[i]
if stack.GitConfig == nil {
continue
}
stack.GitConfig.ConfigFilePath = stack.EntryPoint
if err := stackService.UpdateStack(stack.ID, stack); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,12 @@
package migrator
func (m *Migrator) migrateDBVersionToDB35() error {
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
// calling it again will now fix the issue as the function has been repaired.
migrateLog.Info("Updating dockerhub registries")
err := m.updateDockerhubToDB32()
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,94 @@
package migrator
const (
db35TestFile = "portainer-mig-35.db"
username = "portainer"
password = "password"
)
// TODO: this is exactly the kind of reaching into the internals of the store we should not do
// func setupDB35Test(t *testing.T) *Migrator {
// is := assert.New(t)
// dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
// is.NoError(err, "failed to init testing DB connection")
// // Create an old style dockerhub authenticated account
// dockerhubService, err := dockerhub.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init testing registry service")
// err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
// is.NoError(err, "failed to create dockerhub account")
// registryService, err := registry.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init testing registry service")
// endpointService, err := endpoint.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init endpoint service")
// m := &Migrator{
// db: dbConn,
// dockerhubService: dockerhubService,
// registryService: registryService,
// endpointService: endpointService,
// }
// return m
// }
// // TestUpdateDockerhubToDB32 tests a normal upgrade
// func TestUpdateDockerhubToDB32(t *testing.T) {
// is := assert.New(t)
// m := setupDB35Test(t)
// defer m.db.Close()
// defer os.Remove(db35TestFile)
// if err := m.updateDockerhubToDB32(); err != nil {
// t.Errorf("failed to update settings: %v", err)
// }
// // Verify we have a single registry were created
// registries, err := m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Equal(len(registries), 1, "only one migrated registry expected")
// }
// // TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
// // created a large number of duplicate "dockerhub migrated" registry entries.
// func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
// is := assert.New(t)
// m := setupDB35Test(t)
// defer m.db.Close()
// defer os.Remove(db35TestFile)
// // Create lots of duplicate entries...
// registry := &portainer.Registry{
// Type: portainer.DockerHubRegistry,
// Name: "Dockerhub (authenticated - migrated)",
// URL: "docker.io",
// Authentication: true,
// Username: "portainer",
// Password: "password",
// RegistryAccesses: portainer.RegistryAccesses{},
// }
// for i := 1; i < 150; i++ {
// err := m.registryService.CreateRegistry(registry)
// assert.NoError(t, err, "create registry failed")
// }
// // Verify they were created
// registries, err := m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Condition(func() bool {
// return len(registries) > 1
// }, "expected multiple duplicate registry entries")
// // Now run the migrator
// if err := m.updateDockerhubToDB32(); err != nil {
// t.Errorf("failed to update settings: %v", err)
// }
// // Verify we have a single registry were created
// registries, err = m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Equal(len(registries), 1, "only one migrated registry expected")
// }

View file

@ -0,0 +1,100 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
"github.com/portainer/portainer/api/dataservices/schedule"
"github.com/portainer/portainer/api/dataservices/settings"
"github.com/portainer/portainer/api/dataservices/stack"
"github.com/portainer/portainer/api/dataservices/tag"
"github.com/portainer/portainer/api/dataservices/teammembership"
"github.com/portainer/portainer/api/dataservices/user"
"github.com/portainer/portainer/api/dataservices/version"
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/portainer/portainer/api/internal/authorization"
)
var migrateLog = plog.NewScopedLog("database, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
scheduleService *schedule.Service
settingsService *settings.Service
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
userService *user.Service
versionService *version.Service
fileService portainer.FileService
authorizationService *authorization.Service
dockerhubService *dockerhub.Service
}
// MigratorParameters represents the required parameters to create a new Migrator instance.
MigratorParameters struct {
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
AuthorizationService *authorization.Service
DockerhubService *dockerhub.Service
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator {
return &Migrator{
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService,
tagService: parameters.TagService,
teamMembershipService: parameters.TeamMembershipService,
stackService: parameters.StackService,
userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService,
}
}
// Version exposes version of database
func (migrator *Migrator) Version() int {
return migrator.currentDBVersion
}

579
api/datastore/services.go Normal file
View file

@ -0,0 +1,579 @@
package datastore
import (
"encoding/json"
"io/ioutil"
"strconv"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/dataservices/apikeyrepository"
"github.com/portainer/portainer/api/dataservices/customtemplate"
"github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
"github.com/portainer/portainer/api/dataservices/schedule"
"github.com/portainer/portainer/api/dataservices/settings"
"github.com/portainer/portainer/api/dataservices/ssl"
"github.com/portainer/portainer/api/dataservices/stack"
"github.com/portainer/portainer/api/dataservices/tag"
"github.com/portainer/portainer/api/dataservices/team"
"github.com/portainer/portainer/api/dataservices/teammembership"
"github.com/portainer/portainer/api/dataservices/tunnelserver"
"github.com/portainer/portainer/api/dataservices/user"
"github.com/portainer/portainer/api/dataservices/version"
"github.com/portainer/portainer/api/dataservices/webhook"
"github.com/sirupsen/logrus"
)
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
connection portainer.Connection
fileService portainer.FileService
CustomTemplateService *customtemplate.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
APIKeyRepositoryService *apikeyrepository.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
SSLSettingsService *ssl.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TeamService *team.Service
TunnelServerService *tunnelserver.Service
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
}
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.connection)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.connection)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.connection)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.connection)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.connection)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.connection)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.connection)
if err != nil {
return err
}
store.ExtensionService = extensionService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
}
store.HelmUserRepositoryService = helmUserRepositoryService
registryService, err := registry.NewService(store.connection)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.connection)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.connection)
if err != nil {
return err
}
store.SettingsService = settingsService
sslSettingsService, err := ssl.NewService(store.connection)
if err != nil {
return err
}
store.SSLSettingsService = sslSettingsService
stackService, err := stack.NewService(store.connection)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.connection)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.connection)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.connection)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.connection)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.connection)
if err != nil {
return err
}
store.UserService = userService
apiKeyService, err := apikeyrepository.NewService(store.connection)
if err != nil {
return err
}
store.APIKeyRepositoryService = apiKeyService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.connection)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.connection)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() dataservices.CustomTemplateService {
return store.CustomTemplateService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() dataservices.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() dataservices.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() dataservices.EdgeStackService {
return store.EdgeStackService
}
// Environment(Endpoint) gives access to the Environment(Endpoint) data management layer
func (store *Store) Endpoint() dataservices.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() dataservices.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() dataservices.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() dataservices.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() dataservices.RoleService {
return store.RoleService
}
// APIKeyRepository gives access to the api-key data management layer
func (store *Store) APIKeyRepository() dataservices.APIKeyRepository {
return store.APIKeyRepositoryService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() dataservices.SettingsService {
return store.SettingsService
}
// SSLSettings gives access to the SSL Settings data management layer
func (store *Store) SSLSettings() dataservices.SSLSettingsService {
return store.SSLSettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() dataservices.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() dataservices.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() dataservices.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() dataservices.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() dataservices.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() dataservices.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() dataservices.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() dataservices.WebhookService {
return store.WebhookService
}
type storeExport struct {
CustomTemplate []portainer.CustomTemplate `json:"customtemplates,omitempty"`
EdgeGroup []portainer.EdgeGroup `json:"edgegroups,omitempty"`
EdgeJob []portainer.EdgeJob `json:"edgejobs,omitempty"`
EdgeStack []portainer.EdgeStack `json:"edge_stack,omitempty"`
Endpoint []portainer.Endpoint `json:"endpoints,omitempty"`
EndpointGroup []portainer.EndpointGroup `json:"endpoint_groups,omitempty"`
EndpointRelation []portainer.EndpointRelation `json:"endpoint_relations,omitempty"`
Extensions []portainer.Extension `json:"extension,omitempty"`
HelmUserRepository []portainer.HelmUserRepository `json:"helm_user_repository,omitempty"`
Registry []portainer.Registry `json:"registries,omitempty"`
ResourceControl []portainer.ResourceControl `json:"resource_control,omitempty"`
Role []portainer.Role `json:"roles,omitempty"`
Schedules []portainer.Schedule `json:"schedules,omitempty"`
Settings portainer.Settings `json:"settings,omitempty"`
SSLSettings portainer.SSLSettings `json:"ssl,omitempty"`
Stack []portainer.Stack `json:"stacks,omitempty"`
Tag []portainer.Tag `json:"tags,omitempty"`
TeamMembership []portainer.TeamMembership `json:"team_membership,omitempty"`
Team []portainer.Team `json:"teams,omitempty"`
TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitempty"`
User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().CustomTemplates(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.CustomTemplate = c
}
if e, err := store.EdgeGroup().EdgeGroups(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeGroup = e
}
if e, err := store.EdgeJob().EdgeJobs(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeJob = e
}
if e, err := store.EdgeStack().EdgeStacks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EdgeStack = e
}
if e, err := store.Endpoint().Endpoints(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Endpoint = e
}
if e, err := store.EndpointGroup().EndpointGroups(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointGroup = e
}
if r, err := store.EndpointRelation().EndpointRelations(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.EndpointRelation = r
}
if r, err := store.ExtensionService.Extensions(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Extensions = r
}
if r, err := store.HelmUserRepository().HelmUserRepositorys(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.HelmUserRepository = r
}
if r, err := store.Registry().Registries(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Registry = r
}
if c, err := store.ResourceControl().ResourceControls(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.ResourceControl = c
}
if role, err := store.Role().Roles(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Role = role
}
if r, err := store.ScheduleService.Schedules(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Schedules = r
}
if settings, err := store.Settings().Settings(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Settings = *settings
}
if settings, err := store.SSLSettings().Settings(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.SSLSettings = *settings
}
if t, err := store.Stack().Stacks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Stack = t
}
if t, err := store.Tag().Tags(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Tag = t
}
if t, err := store.TeamMembership().TeamMemberships(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TeamMembership = t
}
if t, err := store.Team().Teams(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Team = t
}
if info, err := store.TunnelServer().Info(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.TunnelServer = *info
}
if users, err := store.User().Users(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.User = users
}
if webhooks, err := store.Webhook().Webhooks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
} else {
backup.Webhook = webhooks
}
v, err := store.Version().DBVersion()
if err != nil {
logrus.WithError(err).Debugf("Export boom")
}
instance, _ := store.Version().InstanceID()
backup.Version = map[string]string{
"DB_VERSION": strconv.Itoa(v),
"INSTANCE_ID": instance,
}
b, err := json.MarshalIndent(backup, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(filename, b, 0600)
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
err = json.Unmarshal([]byte(s), &backup)
if err != nil {
return err
}
// TODO: yup, this is bad, and should be in a version struct...
if dbversion, ok := backup.Version["DB_VERSION"]; ok {
if v, err := strconv.Atoi(dbversion); err == nil {
if err := store.Version().StoreDBVersion(v); err != nil {
logrus.WithError(err).Errorf("DB_VERSION import issue")
}
}
}
if instanceID, ok := backup.Version["INSTANCE_ID"]; ok {
if err := store.Version().StoreInstanceID(instanceID); err != nil {
logrus.WithError(err).Errorf("INSTANCE_ID import issue")
}
}
for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)
}
for _, v := range backup.EdgeGroup {
store.EdgeGroup().UpdateEdgeGroup(v.ID, &v)
}
for _, v := range backup.EdgeJob {
store.EdgeJob().UpdateEdgeJob(v.ID, &v)
}
for _, v := range backup.EdgeStack {
store.EdgeStack().UpdateEdgeStack(v.ID, &v)
}
for _, v := range backup.Endpoint {
store.Endpoint().UpdateEndpoint(v.ID, &v)
}
for _, v := range backup.EndpointGroup {
store.EndpointGroup().UpdateEndpointGroup(v.ID, &v)
}
for _, v := range backup.EndpointRelation {
store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v)
}
for _, v := range backup.HelmUserRepository {
store.HelmUserRepository().UpdateHelmUserRepository(v.ID, &v)
}
for _, v := range backup.Registry {
store.Registry().UpdateRegistry(v.ID, &v)
}
for _, v := range backup.ResourceControl {
store.ResourceControl().UpdateResourceControl(v.ID, &v)
}
for _, v := range backup.Role {
store.Role().UpdateRole(v.ID, &v)
}
store.Settings().UpdateSettings(&backup.Settings)
store.SSLSettings().UpdateSettings(&backup.SSLSettings)
for _, v := range backup.Stack {
store.Stack().UpdateStack(v.ID, &v)
}
for _, v := range backup.Tag {
store.Tag().UpdateTag(v.ID, &v)
}
for _, v := range backup.TeamMembership {
store.TeamMembership().UpdateTeamMembership(v.ID, &v)
}
for _, v := range backup.Team {
store.Team().UpdateTeam(v.ID, &v)
}
store.TunnelServer().UpdateInfo(&backup.TunnelServer)
for _, user := range backup.User {
if err := store.User().UpdateUser(user.ID, &user); err != nil {
logrus.WithField("user", user).WithError(err).Errorf("User: Failed to Update Database")
}
}
// backup[store.Webhook().BucketName()], err = store.Webhook().Webhooks()
// if err != nil {
// logrus.WithError(err).Debugf("Export boom")
// }
return nil
}

View file

@ -0,0 +1,87 @@
package datastore
import (
"io/ioutil"
"log"
"os"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/filesystem"
)
var errTempDir = errors.New("can't create a temp dir")
func (store *Store) GetConnection() portainer.Connection {
return store.connection
}
func MustNewTestStore(init bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
}
log.Fatal(err)
}
return newStore, store, teardown
}
func NewTestStore(init bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
storePath, err := ioutil.TempDir("", "test-store")
if err != nil {
return false, nil, nil, errors.Wrap(errTempDir, err.Error())
}
fileService, err := filesystem.NewService(storePath, "")
if err != nil {
return false, nil, nil, err
}
connection, err := database.NewDatabase("boltdb", storePath)
if err != nil {
panic(err)
}
store := NewStore(storePath, fileService, connection)
newStore, err := store.Open()
if err != nil {
return newStore, nil, nil, err
}
if init {
err = store.Init()
if err != nil {
return newStore, nil, nil, err
}
}
if newStore {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return newStore, nil, nil, err
}
}
teardown := func() {
teardown(store, storePath)
}
return newStore, store, teardown, nil
}
func teardown(store *Store, storePath string) {
err := store.Close()
if err != nil {
log.Fatalln(err)
}
err = os.RemoveAll(storePath)
if err != nil {
log.Fatalln(err)
}
}