chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Backup takes an optional output path and creates a backup of the database.
|
||||
// The database connection is stopped before running the backup to avoid any
|
||||
// corruption and if a path is not given a default is used.
|
||||
// The path or an error are returned.
|
||||
func (store *Store) Backup(path string) (string, error) {
|
||||
if err := store.Close(); err != nil {
|
||||
return "", fmt.Errorf("failed to close store before backup: %w", err)
|
||||
}
|
||||
|
||||
filename, err := store.backupDBFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := store.Open(); err != nil {
|
||||
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// backupDBFile copies the database file to the backup location.
|
||||
// Does not manage connection state - works with the database file directly regardless of connection state.
|
||||
func (store *Store) backupDBFile(backupPath string) (string, error) {
|
||||
if err := store.createBackupPath(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
backupFilename := store.backupFilename()
|
||||
if backupPath != "" {
|
||||
backupFilename = backupPath
|
||||
}
|
||||
|
||||
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msg("Backing up database")
|
||||
|
||||
if err := store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true); err != nil {
|
||||
return "", fmt.Errorf("failed to create backup file: %w", err)
|
||||
}
|
||||
|
||||
return backupFilename, nil
|
||||
}
|
||||
|
||||
func (store *Store) Restore() error {
|
||||
backupFilename := store.backupFilename()
|
||||
return store.RestoreFromFile(backupFilename)
|
||||
}
|
||||
|
||||
func (store *Store) RestoreFromFile(backupFilename string) error {
|
||||
if err := store.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil {
|
||||
return fmt.Errorf("unable to restore backup file %q. err: %w", backupFilename, err)
|
||||
}
|
||||
|
||||
log.Info().Str("from", backupFilename).Str("to", store.connection.GetDatabaseFilePath()).Msgf("database restored")
|
||||
|
||||
if _, err := store.Open(); err != nil {
|
||||
return fmt.Errorf("unable to determine version of restored portainer backup file: %w", err)
|
||||
}
|
||||
|
||||
// determine the db version
|
||||
version, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine restored database version. err: %w", err)
|
||||
}
|
||||
|
||||
editionLabel := portainer.SoftwareEdition(version.Edition).GetEditionLabel()
|
||||
log.Info().Msgf("Restored database version: Portainer %s %s", editionLabel, version.SchemaVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) createBackupPath() error {
|
||||
backupDir := path.Join(store.connection.GetStorePath(), "backups")
|
||||
if exists, _ := store.fileService.FileExists(backupDir); !exists {
|
||||
if err := os.MkdirAll(backupDir, 0o700); err != nil {
|
||||
return fmt.Errorf("unable to create backup folder: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) backupFilename() string {
|
||||
return path.Join(store.connection.GetStorePath(), "backups", store.connection.GetDatabaseFileName()+".bak")
|
||||
}
|
||||
|
||||
func (store *Store) databasePath() string {
|
||||
return store.connection.GetDatabaseFilePath()
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, true)
|
||||
require.NotNil(t, store)
|
||||
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
if portainer.SoftwareEdition(v.Edition) != portainer.PortainerCE {
|
||||
t.Error("Expect to get CE Edition")
|
||||
}
|
||||
|
||||
if v.SchemaVersion != portainer.APIVersion {
|
||||
t.Error("Expect to get APIVersion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, true)
|
||||
backupFileName := store.backupFilename()
|
||||
t.Run("Backup should create "+backupFileName, func(t *testing.T) {
|
||||
v := models.Version{
|
||||
Edition: int(portainer.PortainerCE),
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
}
|
||||
|
||||
err := store.VersionService.UpdateVersion(&v)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.Backup("")
|
||||
require.NoError(t, err)
|
||||
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRestore(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
t.Run("Basic Restore", func(t *testing.T) {
|
||||
// override and set initial db version and edition
|
||||
updateEdition(store, portainer.PortainerCE)
|
||||
updateVersion(store, "2.4")
|
||||
|
||||
_, err := store.Backup("")
|
||||
require.NoError(t, err)
|
||||
|
||||
updateVersion(store, "2.16")
|
||||
testVersion(store, "2.16", t)
|
||||
|
||||
err = store.Restore()
|
||||
require.NoError(t, err)
|
||||
|
||||
// check if the restore is successful and the version is correct
|
||||
testVersion(store, "2.4", t)
|
||||
})
|
||||
|
||||
t.Run("Basic Restore After Multiple Backups", func(t *testing.T) {
|
||||
// override and set initial db version and edition
|
||||
updateEdition(store, portainer.PortainerCE)
|
||||
updateVersion(store, "2.4")
|
||||
|
||||
_, err := store.Backup("")
|
||||
require.NoError(t, err)
|
||||
|
||||
updateVersion(store, "2.14")
|
||||
updateVersion(store, "2.16")
|
||||
testVersion(store, "2.16", t)
|
||||
|
||||
err = store.Restore()
|
||||
require.NoError(t, err)
|
||||
|
||||
// check if the restore is successful and the version is correct
|
||||
testVersion(store, "2.4", t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackupDBFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
t.Run("creates backup file without managing connection state", func(t *testing.T) {
|
||||
// Verify connection is usable before
|
||||
_, err := store.VersionService.Version()
|
||||
require.NoError(t, err, "connection should be usable before backupDBFile")
|
||||
|
||||
// backupDBFile should work without closing the connection
|
||||
backupFilename, err := store.backupDBFile("")
|
||||
require.NoError(t, err)
|
||||
require.FileExists(t, backupFilename)
|
||||
|
||||
// Verify connection is still usable after (not closed/reopened)
|
||||
_, err = store.VersionService.Version()
|
||||
require.NoError(t, err, "connection should still be usable after backupDBFile")
|
||||
|
||||
require.NoError(t, os.Remove(backupFilename))
|
||||
})
|
||||
|
||||
t.Run("uses custom path when provided", func(t *testing.T) {
|
||||
customPath := t.TempDir() + "/custom-backup.db"
|
||||
backupFilename, err := store.backupDBFile(customPath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, customPath, backupFilename)
|
||||
require.FileExists(t, backupFilename)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackupDBFileUsesCorrectPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
t.Run("backs up unencrypted db when encrypted flag is false", func(t *testing.T) {
|
||||
err := store.connection.SetEncrypted(false)
|
||||
require.NoError(t, err)
|
||||
|
||||
backupFilename, err := store.backupDBFile("")
|
||||
require.NoError(t, err)
|
||||
require.FileExists(t, backupFilename)
|
||||
|
||||
// Verify it backed up the unencrypted file (portainer.db)
|
||||
require.Contains(t, backupFilename, boltdb.DatabaseFileName)
|
||||
require.NotContains(t, backupFilename, boltdb.EncryptedDatabaseFileName)
|
||||
|
||||
require.NoError(t, os.Remove(backupFilename))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(cliFlags *portainer.CLIFlags, fileService portainer.FileService, connection portainer.Connection) *Store {
|
||||
return &Store{
|
||||
flags: cliFlags,
|
||||
fileService: fileService,
|
||||
connection: connection,
|
||||
}
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (store *Store) Open() (newStore bool, err error) {
|
||||
encryptionReq, err := store.connection.NeedsEncryptionMigration()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if encryptionReq {
|
||||
// NeedsEncryptionMigration() sets encrypted=true as a side effect when a key exists.
|
||||
// We need to set it back to false so GetDatabaseFilePath() returns the path to the
|
||||
// actual unencrypted file (portainer.db) that we want to back up.
|
||||
if err := store.connection.SetEncrypted(false); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Use backupDBFile directly since connection isn't open yet
|
||||
// and we don't want to trigger the close/open cycle of Backup()
|
||||
backupFilename, err := store.backupDBFile("")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to backup database prior to encrypting: %w", err)
|
||||
}
|
||||
|
||||
if err := store.encryptDB(); err != nil {
|
||||
innerErr := store.RestoreFromFile(backupFilename) // restore from backup if encryption fails
|
||||
return false, errors.Join(err, innerErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.connection.Open(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := store.initServices(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If no settings object exists then assume we have a new store
|
||||
if _, err := store.SettingsService.Settings(); err != nil {
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (store *Store) Close() error {
|
||||
return store.connection.Close()
|
||||
}
|
||||
|
||||
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
|
||||
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return fn(&StoreTx{store: store, tx: tx})
|
||||
})
|
||||
}
|
||||
|
||||
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
|
||||
return store.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return fn(&StoreTx{store: store, tx: tx})
|
||||
})
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (store *Store) BackupTo(w io.Writer) error {
|
||||
return store.connection.BackupTo(w)
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.Edition {
|
||||
return portainerErrors.ErrWrongDBEdition
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
edition = portainer.PortainerCE
|
||||
}
|
||||
|
||||
return edition
|
||||
}
|
||||
|
||||
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
|
||||
func (store *Store) IsErrObjectNotFound(e error) bool {
|
||||
return errors.Is(e, portainerErrors.ErrObjectNotFound)
|
||||
}
|
||||
|
||||
func (store *Store) Connection() portainer.Connection {
|
||||
return store.connection
|
||||
}
|
||||
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
return store.connectionRollback(force)
|
||||
}
|
||||
|
||||
func (store *Store) encryptDB() error {
|
||||
if err := store.connection.SetEncrypted(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.connection.Open(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.initServices(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The DB is not currently encrypted. First save the encrypted db filename
|
||||
oldFilename := store.connection.GetDatabaseFilePath()
|
||||
log.Info().Msg("encrypting database")
|
||||
|
||||
// export file path for backup
|
||||
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
|
||||
|
||||
log.Info().Str("filename", exportFilename).Msg("exporting database backup")
|
||||
|
||||
if err := store.Export(exportFilename); err != nil {
|
||||
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Msg("database backup exported")
|
||||
|
||||
// Close existing un-encrypted db so that we can delete the file later
|
||||
if err := store.connection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.Import(exportFilename); err != nil {
|
||||
log.Error().Err(err).Msg("failed to import database backup")
|
||||
|
||||
// Remove the new encrypted file that we failed to import
|
||||
if err := os.Remove(store.connection.GetDatabaseFilePath()); err != nil {
|
||||
log.Error().Msg("failed to remove the file after import failure")
|
||||
}
|
||||
|
||||
log.Fatal().Err(portainerErrors.ErrDBImportFailed).Msg("")
|
||||
}
|
||||
|
||||
if err := os.Remove(oldFilename); err != nil {
|
||||
log.Error().Msg("failed to remove the un-encrypted db file")
|
||||
}
|
||||
|
||||
if err := os.Remove(exportFilename); err != nil {
|
||||
log.Error().Msg("failed to remove the json backup file")
|
||||
}
|
||||
|
||||
// Close db connection
|
||||
if err := store.connection.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Msg("database successfully encrypted")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, true, true)
|
||||
|
||||
testCases := map[string]func(t *testing.T){
|
||||
"User Accounts": store.testUserAccounts,
|
||||
"Environments": store.testEnvironments,
|
||||
"Settings": store.testSettings,
|
||||
"SSL Settings": store.testSSLSettings,
|
||||
"Tunnel Server": store.testTunnelServer,
|
||||
"Custom Templates": store.testCustomTemplates,
|
||||
"Registries": store.testRegistries,
|
||||
"Resource Control": store.testResourceControl,
|
||||
"Schedules": store.testSchedules,
|
||||
"Tags": store.testTags,
|
||||
}
|
||||
|
||||
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(t, id)
|
||||
|
||||
id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
|
||||
store.CreateEndpointRelation(t, id)
|
||||
|
||||
id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
|
||||
store.CreateEndpointRelation(t, 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,
|
||||
},
|
||||
TagIDs: nil,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: nil,
|
||||
Kubernetes: portainer.KubernetesData{
|
||||
Configuration: portainer.KubernetesConfiguration{
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
EnableResourceOverCommit: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if TLS {
|
||||
endpoint.TLSConfig = portainer.TLSConfiguration{
|
||||
TLS: true,
|
||||
TLSSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
|
||||
endpoint.SecuritySettings = portainer.DefaultEndpointSecuritySettings()
|
||||
}
|
||||
|
||||
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, 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)
|
||||
|
||||
err := store.Endpoint().Create(expectedEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
endpoint, err := store.Endpoint().Endpoint(id)
|
||||
require.NoError(t, err, "Endpoint() should not return an error")
|
||||
is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
|
||||
|
||||
return endpoint.ID
|
||||
}
|
||||
|
||||
func (store *Store) CreateEndpointRelation(t *testing.T, id portainer.EndpointID) {
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: id,
|
||||
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||
}
|
||||
|
||||
err := store.EndpointRelation().Create(relation)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
err := store.SSLSettings().UpdateSettings(ssl)
|
||||
require.NoError(t, err)
|
||||
|
||||
settings, err := store.SSLSettings().Settings()
|
||||
require.NoError(t, 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})
|
||||
require.NoError(t, err, "UpdateInfo should have succeeded")
|
||||
|
||||
serverInfo, err := store.TunnelServer().Info()
|
||||
require.NoError(t, 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) {
|
||||
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
|
||||
require.NoError(t, err, "CreateAccount should succeed")
|
||||
|
||||
err = store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
|
||||
require.NoError(t, err, "Account failure")
|
||||
|
||||
err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
|
||||
require.NoError(t, err, "CreateAccount should succeed")
|
||||
|
||||
err = store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
|
||||
require.NoError(t, 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
|
||||
}
|
||||
|
||||
return store.User().Create(user)
|
||||
}
|
||||
|
||||
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{}
|
||||
if cs.CompareHashAndData(user.Password, expectPassword) != nil {
|
||||
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()
|
||||
require.NoError(t, 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)
|
||||
require.NoError(t, err, "UpdateSettings() should succeed")
|
||||
|
||||
settings, err := store.Settings().Settings()
|
||||
require.NoError(t, 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,
|
||||
}
|
||||
|
||||
err := customTemplate.Create(expectedTemplate)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualTemplate, err := customTemplate.Read(expectedTemplate.ID)
|
||||
require.NoError(t, 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)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = regService.Create(reg2)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualReg1, err := regService.Read(reg1.ID)
|
||||
require.NoError(t, err)
|
||||
is.Equal(reg1, actualReg1, "registries differ")
|
||||
|
||||
actualReg2, err := regService.Read(reg2.ID)
|
||||
require.NoError(t, 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)
|
||||
require.NoError(t, err, "CreateSchedule should succeed")
|
||||
|
||||
actual, err := schedule.Schedule(s.ID)
|
||||
require.NoError(t, 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)
|
||||
require.NoError(t, err, "Tags.Create should succeed")
|
||||
|
||||
err = tags.Create(tag2)
|
||||
require.NoError(t, err, "Tags.Create should succeed")
|
||||
|
||||
actual, err := tags.Read(tag1.ID)
|
||||
require.NoError(t, err, "tag1 should be found")
|
||||
is.Equal(tag1, actual, "tags differ")
|
||||
|
||||
actual, err = tags.Read(tag2.ID)
|
||||
require.NoError(t, err, "tag2 should be found")
|
||||
is.Equal(tag2, actual, "tags differ")
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// isFileExist is helper function to check for file existence
|
||||
func isFileExist(path string) bool {
|
||||
matches, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(matches) > 0
|
||||
}
|
||||
|
||||
func updateVersion(store *Store, v string) {
|
||||
version, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
version.SchemaVersion = v
|
||||
|
||||
err = store.VersionService.UpdateVersion(version)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
}
|
||||
|
||||
func updateEdition(store *Store, edition portainer.SoftwareEdition) {
|
||||
version, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
version.Edition = int(edition)
|
||||
|
||||
err = store.VersionService.UpdateVersion(version)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
}
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
func testVersion(store *Store, versionWant string, t *testing.T) {
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
if v.SchemaVersion != versionWant {
|
||||
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Init creates the default data set.
|
||||
func (store *Store) Init() error {
|
||||
err := store.checkOrCreateDefaultSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.checkOrCreateDefaultSSLSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.checkOrCreateDefaultData()
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
isDDExtention := false
|
||||
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
||||
isDDExtention = true
|
||||
}
|
||||
|
||||
// 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),
|
||||
InternalAuthSettings: portainer.InternalAuthSettings{
|
||||
RequiredPasswordLength: 12,
|
||||
},
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
{},
|
||||
},
|
||||
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
||||
{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{
|
||||
SSO: true,
|
||||
},
|
||||
SnapshotInterval: portainer.DefaultSnapshotInterval,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: "",
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
KubectlShellImage: *store.flags.KubectlShellImage,
|
||||
|
||||
IsDockerDesktopExtension: isDDExtention,
|
||||
EnforceEdgeID: true,
|
||||
}
|
||||
|
||||
return store.SettingsService.UpdateSettings(defaultSettings)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.UserSessionTimeout == "" {
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
return store.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateDefaultSSLSettings() error {
|
||||
_, err := store.SSLSettings().Settings()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
defaultSSLSettings := &portainer.SSLSettings{
|
||||
HTTPEnabled: true,
|
||||
}
|
||||
|
||||
return store.SSLSettings().UpdateSettings(defaultSSLSettings)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateDefaultData() error {
|
||||
groups, err := store.EndpointGroupService.ReadAll()
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (store *Store) MigrateData() error {
|
||||
updating, err := store.VersionService.IsUpdating()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while checking if the store is updating")
|
||||
}
|
||||
|
||||
if updating {
|
||||
return dserrors.ErrDatabaseIsUpdating
|
||||
}
|
||||
|
||||
// migrate new version bucket if required (doesn't write anything to db yet)
|
||||
version, err := store.getOrMigrateLegacyVersion()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while migrating legacy version")
|
||||
}
|
||||
|
||||
migratorParams := store.newMigratorParameters(version, store.flags)
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
if !migrator.NeedsMigration() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// before we alter anything in the DB, create a backup
|
||||
if _, err := store.Backup(""); err != nil {
|
||||
return errors.Wrap(err, "while backing up database")
|
||||
}
|
||||
|
||||
if err := store.FailSafeMigrate(migrator, version); err != nil {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
||||
restoreErr := store.Restore()
|
||||
if restoreErr != nil {
|
||||
return errors.Wrap(restoreErr, "failed to restore database")
|
||||
}
|
||||
|
||||
log.Info().Msg("database restored to previous version")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) newMigratorParameters(version *models.Version, flags *portainer.CLIFlags) *migrator.MigratorParameters {
|
||||
return &migrator.MigratorParameters{
|
||||
Flags: flags,
|
||||
CurrentDBVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
SnapshotService: store.SnapshotService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EdgeStackStatusService: store.EdgeStackStatusService,
|
||||
EdgeJobService: store.EdgeJobService,
|
||||
EdgeGroupService: store.EdgeGroupService,
|
||||
TunnelServerService: store.TunnelServerService,
|
||||
PendingActionsService: store.PendingActionsService,
|
||||
CustomTemplateService: store.CustomTemplateService,
|
||||
SourceService: store.SourceService,
|
||||
WorkflowService: store.WorkflowService,
|
||||
}
|
||||
}
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
|
||||
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
err = store.VersionService.StoreIsUpdating(true)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while updating the store")
|
||||
}
|
||||
|
||||
// now update the version to the new struct (if required)
|
||||
err = store.finishMigrateLegacyVersion(version)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while updating version")
|
||||
}
|
||||
|
||||
log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portainer.APIVersion)
|
||||
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Special test code to simulate a failure (used by migrate_data_test.go). Do not remove...
|
||||
if os.Getenv("PORTAINER_TEST_MIGRATE_FAIL") == "FAIL" {
|
||||
panic("test migration failure")
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreIsUpdating(false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update the store")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.Restore(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.connection.Close()
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
srcPath string
|
||||
wantPath string
|
||||
overrideInstanceId bool
|
||||
}{
|
||||
{
|
||||
testName: "migrate version 24 to latest",
|
||||
srcPath: "test_data/input_24.json",
|
||||
wantPath: "test_data/output_24_to_latest.json",
|
||||
overrideInstanceId: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
||||
if err != nil {
|
||||
t.Errorf(
|
||||
"Failed migrating mock database %v: %v",
|
||||
test.srcPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
newStore, store := MustNewTestStore(t, true, false)
|
||||
if !newStore {
|
||||
t.Error("Expect a new DB")
|
||||
}
|
||||
|
||||
testVersion(store, portainer.APIVersion, t)
|
||||
err := store.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
newStore, err = store.Open()
|
||||
require.NoError(t, err)
|
||||
if newStore {
|
||||
t.Error("Expect store to NOT be new DB")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
err := store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "2.0", Edition: int(portainer.PortainerCE)})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.MigrateData()
|
||||
require.NoError(t, err)
|
||||
|
||||
backupfilename := store.backupFilename()
|
||||
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
|
||||
t.Errorf("Expect backup file to be created %s", backupfilename)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) {
|
||||
t.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL")
|
||||
|
||||
version := "2.15"
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
err := store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.MigrateData()
|
||||
require.Error(t, err)
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
err := store.VersionService.StoreIsUpdating(true)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.MigrateData()
|
||||
require.Error(t, err)
|
||||
|
||||
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||
// only create a backup when the version changes.
|
||||
backupfilename := store.backupFilename()
|
||||
if exists, _ := store.fileService.FileExists(backupfilename); exists {
|
||||
t.Errorf("Backup file should not exist for dirty database")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
// Set migrator the count to match our migrations array (simulate no changes).
|
||||
// Should not create a backup
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
t.Errorf("Unable to read version from db: %s", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
migratorParams := store.newMigratorParameters(v, store.flags)
|
||||
m := migrator.NewMigrator(migratorParams)
|
||||
latestMigrations := m.LatestMigrations()
|
||||
|
||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||
v.MigratorCount = len(latestMigrations.MigrationFuncs)
|
||||
err = store.VersionService.UpdateVersion(v)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = store.MigrateData()
|
||||
require.NoError(t, err)
|
||||
|
||||
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||
// only create a backup when the version changes.
|
||||
backupfilename := store.backupFilename()
|
||||
if exists, _ := store.fileService.FileExists(backupfilename); exists {
|
||||
t.Errorf("Backup file should not exist for dirty database")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup on startup if portainer version matches db and migrationFuncs counts differ", func(t *testing.T) {
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
// Set migrator count very large to simulate changes
|
||||
// Should not create a backup
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
t.Errorf("Unable to read version from db: %s", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
v.MigratorCount = 1000
|
||||
|
||||
err = store.VersionService.UpdateVersion(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.MigrateData()
|
||||
require.NoError(t, err)
|
||||
|
||||
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||
// only create a backup when the version changes.
|
||||
backupfilename := store.backupFilename()
|
||||
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
|
||||
t.Errorf("DB backup should exist and there should be no error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := "2.11"
|
||||
|
||||
v := models.Version{SchemaVersion: version}
|
||||
|
||||
_, store := MustNewTestStore(t, false, false)
|
||||
|
||||
err := store.VersionService.UpdateVersion(&v)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.Backup("")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
v.SchemaVersion = "2.14"
|
||||
// Change the current edition
|
||||
err = store.VersionService.UpdateVersion(&v)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
err = store.Rollback(true)
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = store.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := "2.15"
|
||||
|
||||
v := models.Version{
|
||||
SchemaVersion: version,
|
||||
Edition: int(portainer.PortainerCE),
|
||||
}
|
||||
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
err := store.VersionService.UpdateVersion(&v)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.Backup("")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
v.SchemaVersion = "2.14"
|
||||
// Change the current edition
|
||||
err = store.VersionService.UpdateVersion(&v)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
err = store.Rollback(true)
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = store.Open()
|
||||
require.NoError(t, err)
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
}
|
||||
|
||||
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
||||
// parses it into a database, runs a migration on that database, and then
|
||||
// compares it with an expected output database.
|
||||
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
|
||||
srcJSON, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
|
||||
}
|
||||
|
||||
// Parse source json to db.
|
||||
// When we create a new test store, it sets its version field automatically to latest.
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
|
||||
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
|
||||
|
||||
err = store.connection.DeleteObject("version", []byte("VERSION"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// defer teardown()
|
||||
if err := importJSON(t, bytes.NewReader(srcJSON), store); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run the actual migrations on our input database.
|
||||
if err := store.MigrateData(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if overrideInstanceId {
|
||||
// old versions of portainer did not have instance-id. Because this gets generated
|
||||
// we need to override the expected output to match the expected value to pass the test
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
|
||||
if err := store.VersionService.UpdateVersion(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our database connection is using bolt so we can call
|
||||
// exportJson rather than ExportRaw. The exportJson function allows us to
|
||||
// strip out the metadata which we don't want for our tests.
|
||||
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
|
||||
if err := store.connection.Close(); err != nil {
|
||||
t.Fatalf("err closing bolt connection: %v", err)
|
||||
}
|
||||
|
||||
con, ok := store.connection.(*boltdb.DbConnection)
|
||||
if !ok {
|
||||
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
|
||||
}
|
||||
|
||||
// Convert database back to json.
|
||||
databasePath := con.GetDatabaseFilePath()
|
||||
if _, err := os.Stat(databasePath); err != nil {
|
||||
return fmt.Errorf("stat on %s failed: %w", databasePath, err)
|
||||
}
|
||||
|
||||
gotJSON, err := con.ExportJSON(databasePath, false)
|
||||
if err != nil {
|
||||
t.Logf(
|
||||
"failed re-exporting database %s to JSON: %v",
|
||||
databasePath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
wantJSON, err := os.ReadFile(wantPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
|
||||
}
|
||||
|
||||
// Compare the result we got with the one we wanted.
|
||||
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
|
||||
gotPath := filesystem.JoinPaths(os.TempDir(), "portainer-migrator-test-fail.json")
|
||||
err = os.WriteFile(
|
||||
gotPath,
|
||||
gotJSON,
|
||||
0o600,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed writing migrated output to temp file")
|
||||
}
|
||||
|
||||
t.Errorf(
|
||||
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
|
||||
srcPath,
|
||||
wantPath,
|
||||
gotPath,
|
||||
diff,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// importJSON reads input JSON and commits it to a portainer datastore.Store.
|
||||
// Errors are logged with the testing package.
|
||||
func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
||||
objects := make(map[string]any)
|
||||
|
||||
// Parse json into map of objects.
|
||||
d := json.NewDecoder(r)
|
||||
d.UseNumber()
|
||||
err := d.Decode(&objects)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get database connection from store.
|
||||
con := store.connection
|
||||
|
||||
for k, v := range objects {
|
||||
switch k {
|
||||
case "version":
|
||||
versions, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
t.Logf("failed casting %s to map[string]any", k)
|
||||
}
|
||||
|
||||
// New format db
|
||||
version, ok := versions["VERSION"]
|
||||
if ok {
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("VERSION"),
|
||||
version,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing VERSION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
// old format db
|
||||
|
||||
dbVersion, ok := versions["DB_VERSION"]
|
||||
if ok {
|
||||
numDBVersion, ok := dbVersion.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed parsing DB_VERSION as json number from %s", k)
|
||||
}
|
||||
|
||||
intDBVersion, err := numDBVersion.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
|
||||
}
|
||||
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DB_VERSION"),
|
||||
int(intDBVersion),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID, ok := versions["INSTANCE_ID"]
|
||||
if ok {
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INSTANCE_ID"),
|
||||
instanceID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
edition, ok := versions["EDITION"]
|
||||
if ok {
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("EDITION"),
|
||||
edition,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing EDITION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
case "dockerhub":
|
||||
obj, ok := v.([]any)
|
||||
if !ok {
|
||||
t.Logf("failed to cast %s to []any", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DOCKERHUB"),
|
||||
obj[0],
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "ssl":
|
||||
obj, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]any", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("SSL"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing SSL in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "settings":
|
||||
obj, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]any", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("SETTINGS"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing SETTINGS in %s: %v", k, err)
|
||||
}
|
||||
|
||||
case "tunnel_server":
|
||||
obj, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
t.Logf("failed to case %s to map[string]any", k)
|
||||
}
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INFO"),
|
||||
obj,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INFO in %s: %v", k, err)
|
||||
}
|
||||
case "templates":
|
||||
continue
|
||||
|
||||
default:
|
||||
objlist, ok := v.([]any)
|
||||
if !ok {
|
||||
t.Logf("failed to cast %s to []any", k)
|
||||
}
|
||||
|
||||
for _, obj := range objlist {
|
||||
value, ok := obj.(map[string]any)
|
||||
if !ok {
|
||||
t.Logf("failed to cast %v to map[string]any", obj)
|
||||
} else {
|
||||
var ok bool
|
||||
var id any
|
||||
switch k {
|
||||
case "endpoint_relations":
|
||||
// TODO: need to make into an int, then do that weird
|
||||
// stringification
|
||||
id, ok = value["EndpointID"]
|
||||
default:
|
||||
id, ok = value["Id"]
|
||||
}
|
||||
if !ok {
|
||||
// endpoint_relations: EndpointID
|
||||
t.Logf("missing Id field: %s", k)
|
||||
id = "error"
|
||||
}
|
||||
n, ok := id.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed to cast %v to json.Number in %s", id, k)
|
||||
} else {
|
||||
key, err := n.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed to cast %v to int in %s", n, k)
|
||||
} else {
|
||||
err := con.CreateObjectWithId(
|
||||
k,
|
||||
int(key),
|
||||
value,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing %v in %s: %v", key, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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]any) error {
|
||||
//insert a obj
|
||||
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
|
||||
}
|
||||
|
||||
func setup(store *Store) error {
|
||||
dummySettingsObj := map[string]any{
|
||||
"LogoURL": dummyLogoURL,
|
||||
}
|
||||
|
||||
return initTestingSettingsService(store.connection, dummySettingsObj)
|
||||
}
|
||||
|
||||
func TestMigrateSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, false, true)
|
||||
|
||||
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{
|
||||
Flags: store.flags,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, store := MustNewTestStore(t, false, true)
|
||||
|
||||
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)
|
||||
require.NoError(t, err, "failed to create stack")
|
||||
}
|
||||
|
||||
s, err := stackService.Read(1)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, s.GitConfig, "first stack should not have git config")
|
||||
|
||||
s, err = stackService.Read(2)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, s.GitConfig.ConfigFilePath, "not migrated yet migrated")
|
||||
|
||||
err = migrator.MigrateStackEntryPoint(stackService)
|
||||
require.NoError(t, err, "failed to migrate entry point to Git ConfigFilePath")
|
||||
|
||||
s, err = stackService.Read(1)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, s.GitConfig, "first stack should not have git config")
|
||||
|
||||
s, err = stackService.Read(2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "dir/sub/compose.yml", s.GitConfig.ConfigFilePath, "second stack should have config file path migrated")
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketName = "version"
|
||||
legacyDBVersionKey = "DB_VERSION"
|
||||
legacyInstanceKey = "INSTANCE_ID"
|
||||
legacyEditionKey = "EDITION"
|
||||
)
|
||||
|
||||
var dbVerToSemVerMap = map[int]string{
|
||||
18: "1.21",
|
||||
19: "1.22",
|
||||
20: "1.22.1",
|
||||
21: "1.22.2",
|
||||
22: "1.23",
|
||||
23: "1.24",
|
||||
24: "1.24.1",
|
||||
25: "2.0",
|
||||
26: "2.1",
|
||||
27: "2.2",
|
||||
28: "2.4",
|
||||
29: "2.4",
|
||||
30: "2.6",
|
||||
31: "2.7",
|
||||
32: "2.9",
|
||||
33: "2.9.1",
|
||||
34: "2.10",
|
||||
35: "2.9.3",
|
||||
36: "2.11",
|
||||
40: "2.13",
|
||||
50: "2.14",
|
||||
51: "2.14.1",
|
||||
52: "2.14.2",
|
||||
60: "2.15",
|
||||
61: "2.15.1",
|
||||
70: "2.16",
|
||||
80: "2.17",
|
||||
}
|
||||
|
||||
func dbVersionToSemanticVersion(dbVersion int) string {
|
||||
if dbVersion < 18 {
|
||||
return "1.0.0"
|
||||
}
|
||||
|
||||
ver, ok := dbVerToSemVerMap[dbVersion]
|
||||
if ok {
|
||||
return ver
|
||||
}
|
||||
|
||||
// We should always return something sensible
|
||||
switch {
|
||||
case dbVersion < 40:
|
||||
return "2.11"
|
||||
case dbVersion < 50:
|
||||
return "2.13"
|
||||
case dbVersion < 60:
|
||||
return "2.14.2"
|
||||
case dbVersion < 70:
|
||||
return "2.15.1"
|
||||
}
|
||||
|
||||
return "2.16.0"
|
||||
}
|
||||
|
||||
// getOrMigrateLegacyVersion to new Version struct
|
||||
func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
|
||||
// Very old versions of portainer did not have a version bucket, lets set some defaults
|
||||
dbVersion := 24
|
||||
edition := int(portainer.PortainerCE)
|
||||
instanceId := ""
|
||||
|
||||
// If we already have a version key, we don't need to migrate
|
||||
v, err := store.VersionService.Version()
|
||||
if err == nil || !dataservices.IsErrObjectNotFound(err) {
|
||||
return v, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyDBVersionKey), &dbVersion)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyEditionKey), &edition)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyInstanceKey), &instanceId)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.Version{
|
||||
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
|
||||
Edition: edition,
|
||||
InstanceID: instanceId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// finishMigrateLegacyVersion writes the new version to the DB and removes the old version keys from the DB
|
||||
func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) error {
|
||||
if err := store.VersionService.UpdateVersion(versionToWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove legacy keys if present
|
||||
if err := store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.connection.DeleteObject(bucketName, []byte(legacyEditionKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateEdgeStacksStatuses_2_31_0() error {
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for envID, status := range edgeStack.Status {
|
||||
if err := m.edgeStackStatusService.Create(edgeStack.ID, envID, &portainer.EdgeStackStatusForEnv{
|
||||
EndpointID: envID,
|
||||
Status: status.Status,
|
||||
DeploymentInfo: status.DeploymentInfo,
|
||||
ReadyRePullImage: status.ReadyRePullImage,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
edgeStack.Status = nil
|
||||
|
||||
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
perrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
func (m *Migrator) addEndpointRelationForEdgeAgents_2_32_0() error {
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
_, err := m.endpointRelationService.EndpointRelation(endpoint.ID)
|
||||
if err != nil && errors.Is(err, perrors.ErrObjectNotFound) {
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: make(map[portainer.EdgeStackID]bool),
|
||||
}
|
||||
|
||||
if err := m.endpointRelationService.Create(relation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/roar"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateEdgeGroupEndpointsToRoars_2_33_0() error {
|
||||
egs, err := m.edgeGroupService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, eg := range egs {
|
||||
if eg.EndpointIDs.Len() == 0 {
|
||||
eg.EndpointIDs = roar.FromSlice(eg.Endpoints)
|
||||
eg.Endpoints = nil
|
||||
}
|
||||
|
||||
if err := m.edgeGroupService.Update(eg.ID, &eg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices/edgegroup"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrateEdgeGroupEndpointsToRoars_2_33_0Idempotency(t *testing.T) {
|
||||
t.Parallel()
|
||||
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
edgeGroupService, err := edgegroup.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
edgeGroup := &portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "test-edge-group",
|
||||
Endpoints: []portainer.EndpointID{1, 2, 3},
|
||||
}
|
||||
|
||||
err = conn.CreateObjectWithId(edgegroup.BucketName, int(edgeGroup.ID), edgeGroup)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{EdgeGroupService: edgeGroupService})
|
||||
|
||||
// Run migration once
|
||||
|
||||
err = m.migrateEdgeGroupEndpointsToRoars_2_33_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
migratedEdgeGroup, err := edgeGroupService.Read(edgeGroup.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, migratedEdgeGroup.Endpoints)
|
||||
require.Equal(t, len(edgeGroup.Endpoints), migratedEdgeGroup.EndpointIDs.Len())
|
||||
|
||||
// Run migration again to ensure the results didn't change
|
||||
|
||||
err = m.migrateEdgeGroupEndpointsToRoars_2_33_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
migratedEdgeGroup, err = edgeGroupService.Read(edgeGroup.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, migratedEdgeGroup.Endpoints)
|
||||
require.Equal(t, len(edgeGroup.Endpoints), migratedEdgeGroup.EndpointIDs.Len())
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices/endpoint"
|
||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||
"github.com/portainer/portainer/api/dataservices/registry"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrateRegistryAccessSASecrets_2_40_0(t *testing.T) {
|
||||
t.Parallel()
|
||||
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
registryService, err := registry.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
endpointService, err := endpoint.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
pendingActionsService, err := pendingactions.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("sets MigrateRegistrySASecrets flag for k8s endpoints with registry access", func(t *testing.T) {
|
||||
k8sEndpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
Name: "k8s-cluster",
|
||||
Type: portainer.AgentOnKubernetesEnvironment,
|
||||
}
|
||||
dockerEndpoint := &portainer.Endpoint{
|
||||
ID: 2,
|
||||
Name: "docker-standalone",
|
||||
Type: portainer.DockerEnvironment,
|
||||
}
|
||||
|
||||
err := conn.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
|
||||
require.NoError(t, err)
|
||||
err = conn.CreateObjectWithId(endpoint.BucketName, int(dockerEndpoint.ID), dockerEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &portainer.Registry{
|
||||
ID: 1,
|
||||
Name: "test-registry",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
|
||||
Namespaces: []string{"default", "production"},
|
||||
},
|
||||
dockerEndpoint.ID: portainer.RegistryAccessPolicies{
|
||||
Namespaces: []string{"ignored"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = conn.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
RegistryService: registryService,
|
||||
EndpointService: endpointService,
|
||||
PendingActionsService: pendingActionsService,
|
||||
})
|
||||
|
||||
err = m.migrateRegistryAccessSASecrets_2_40_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedK8sEndpoint, err := endpointService.Endpoint(k8sEndpoint.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, updatedK8sEndpoint.PostInitMigrations.MigrateRegistrySASecrets, "should have set MigrateRegistrySASecrets flag for k8s endpoint")
|
||||
|
||||
updatedDockerEndpoint, err := endpointService.Endpoint(dockerEndpoint.ID)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, updatedDockerEndpoint.PostInitMigrations.MigrateRegistrySASecrets, "should not have set MigrateRegistrySASecrets flag for docker endpoint")
|
||||
})
|
||||
|
||||
t.Run("skips endpoints with empty namespaces", func(t *testing.T) {
|
||||
conn2 := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn2.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn2)
|
||||
|
||||
registryService2, _ := registry.NewService(conn2)
|
||||
endpointService2, _ := endpoint.NewService(conn2)
|
||||
pendingActionsService2, _ := pendingactions.NewService(conn2)
|
||||
|
||||
k8sEndpoint := &portainer.Endpoint{
|
||||
ID: 10,
|
||||
Name: "k8s-cluster",
|
||||
Type: portainer.AgentOnKubernetesEnvironment,
|
||||
}
|
||||
err = conn2.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &portainer.Registry{
|
||||
ID: 10,
|
||||
Name: "empty-registry",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
|
||||
Namespaces: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = conn2.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
RegistryService: registryService2,
|
||||
EndpointService: endpointService2,
|
||||
PendingActionsService: pendingActionsService2,
|
||||
})
|
||||
|
||||
err = m.migrateRegistryAccessSASecrets_2_40_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
allPAs, err := pendingActionsService2.ReadAll()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, allPAs, "should not create pending actions for empty namespaces")
|
||||
})
|
||||
|
||||
t.Run("skips non-existent endpoints", func(t *testing.T) {
|
||||
conn3 := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn3.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn3)
|
||||
|
||||
registryService3, _ := registry.NewService(conn3)
|
||||
endpointService3, _ := endpoint.NewService(conn3)
|
||||
pendingActionsService3, _ := pendingactions.NewService(conn3)
|
||||
|
||||
reg := &portainer.Registry{
|
||||
ID: 20,
|
||||
Name: "orphan-registry",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
999: portainer.RegistryAccessPolicies{
|
||||
Namespaces: []string{"default"},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = conn3.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
RegistryService: registryService3,
|
||||
EndpointService: endpointService3,
|
||||
PendingActionsService: pendingActionsService3,
|
||||
})
|
||||
|
||||
err = m.migrateRegistryAccessSASecrets_2_40_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
allPAs, err := pendingActionsService3.ReadAll()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, allPAs, "should not create pending actions for non-existent endpoints")
|
||||
})
|
||||
|
||||
t.Run("idempotent - running twice creates duplicate actions but doesn't error", func(t *testing.T) {
|
||||
conn4 := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn4.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn4)
|
||||
|
||||
registryService4, _ := registry.NewService(conn4)
|
||||
endpointService4, _ := endpoint.NewService(conn4)
|
||||
pendingActionsService4, _ := pendingactions.NewService(conn4)
|
||||
|
||||
k8sEndpoint := &portainer.Endpoint{
|
||||
ID: 30,
|
||||
Name: "k8s-cluster",
|
||||
Type: portainer.AgentOnKubernetesEnvironment,
|
||||
}
|
||||
err = conn4.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &portainer.Registry{
|
||||
ID: 30,
|
||||
Name: "test-registry",
|
||||
RegistryAccesses: portainer.RegistryAccesses{
|
||||
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
|
||||
Namespaces: []string{"default"},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = conn4.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
RegistryService: registryService4,
|
||||
EndpointService: endpointService4,
|
||||
PendingActionsService: pendingActionsService4,
|
||||
})
|
||||
|
||||
err = m.migrateRegistryAccessSASecrets_2_40_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateRegistryAccessSASecrets_2_40_0()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"github.com/portainer/portainer/api/dataservices/stack"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type legacyRepoConfig struct {
|
||||
URL string
|
||||
ReferenceName string
|
||||
ConfigFilePath string
|
||||
Authentication *legacyGitAuthentication
|
||||
ConfigHash string
|
||||
TLSSkipVerify bool
|
||||
}
|
||||
|
||||
type legacyGitAuthentication struct {
|
||||
Username string
|
||||
Password string
|
||||
Provider int `json:",omitempty"`
|
||||
AuthorizationType int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (lrc *legacyRepoConfig) toRepoConfig() *gittypes.RepoConfig {
|
||||
if lrc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &gittypes.RepoConfig{
|
||||
URL: lrc.URL,
|
||||
ReferenceName: lrc.ReferenceName,
|
||||
ConfigFilePath: lrc.ConfigFilePath,
|
||||
ConfigHash: lrc.ConfigHash,
|
||||
TLSSkipVerify: lrc.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if lrc.Authentication != nil {
|
||||
cfg.Authentication = &gittypes.GitAuthentication{
|
||||
Username: lrc.Authentication.Username,
|
||||
Password: lrc.Authentication.Password,
|
||||
Provider: gittypes.GitProvider(lrc.Authentication.Provider),
|
||||
AuthorizationType: gittypes.GitCredentialAuthType(lrc.Authentication.AuthorizationType),
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
type legacyStack struct {
|
||||
ID int `json:"Id"`
|
||||
GitConfig *legacyRepoConfig `json:"GitConfig"`
|
||||
WorkflowID *int
|
||||
ResourceControl *portainer.ResourceControl `json:"ResourceControl"`
|
||||
CreatedBy string
|
||||
}
|
||||
|
||||
// sourceDedupeKey is the identity used to detect duplicate Sources during migration.
|
||||
// Two stacks sharing the same URL and credentials must reuse the same Source record.
|
||||
type sourceDedupeKey struct {
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func gitSourceKey(cfg *gittypes.RepoConfig) sourceDedupeKey {
|
||||
url, err := gittypes.NormalizeURL(gittypes.SanitizeURL(cfg.URL))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("url", cfg.URL).Msg("failed to normalize git URL for deduplication, using raw URL")
|
||||
url = cfg.URL
|
||||
}
|
||||
|
||||
key := sourceDedupeKey{url: url}
|
||||
if cfg.Authentication != nil {
|
||||
key.username = cfg.Authentication.Username
|
||||
key.password = cfg.Authentication.Password
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateGitConfigToSources_2_43_0() error {
|
||||
log.Info().Msg("migrating git-backed stacks to Source+Workflow records")
|
||||
|
||||
var legacyStacks []legacyStack
|
||||
|
||||
err := m.stackService.Connection.GetAll(
|
||||
stack.BucketName,
|
||||
new(legacyStack),
|
||||
func(obj any) (any, error) {
|
||||
s, ok := obj.(*legacyStack)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type reading stack bucket: %T", obj)
|
||||
}
|
||||
|
||||
legacyStacks = append(legacyStacks, *s)
|
||||
|
||||
return new(legacyStack), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUserContext := source.InsecureNewAdminContext()
|
||||
existingSources, err := m.sourceService.ReadAll(adminUserContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourcesByKey := make(map[sourceDedupeKey]portainer.SourceID, len(existingSources))
|
||||
for _, src := range existingSources {
|
||||
if src.Git != nil {
|
||||
sourcesByKey[gitSourceKey(&gittypes.RepoConfig{URL: src.Git.URL, Authentication: src.Git.Authentication})] = src.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, ls := range legacyStacks {
|
||||
if ls.GitConfig == nil || (ls.WorkflowID != nil && *ls.WorkflowID != 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := ls.GitConfig.toRepoConfig()
|
||||
cfg.URL = gittypes.SanitizeURL(cfg.URL)
|
||||
key := gitSourceKey(cfg)
|
||||
|
||||
var newSrcID portainer.SourceID
|
||||
|
||||
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
liveStack, err := m.stackService.Tx(tx).Read(portainer.StackID(ls.ID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read stack %d: %w", ls.ID, err)
|
||||
}
|
||||
|
||||
rc := ls.ResourceControl
|
||||
if rc == nil {
|
||||
rcID := fmt.Sprintf("%d_%s", liveStack.EndpointID, liveStack.Name)
|
||||
rc, err = m.resourceControlService.Tx(tx).ResourceControlByResourceIDAndType(rcID, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read resource control for stack %d: %w", ls.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
users, teams, public, adminOnly, ownerId := GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(rc,
|
||||
func() (portainer.UserID, portainer.UserRole, error) {
|
||||
user, err := m.userService.Tx(tx).UserByUsername(ls.CreatedBy)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return user.ID, user.Role, nil
|
||||
},
|
||||
func(userId portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||
return m.teamMembershipService.Tx(tx).TeamMembershipsByUserID(userId)
|
||||
},
|
||||
)
|
||||
|
||||
srcID, exists := sourcesByKey[key]
|
||||
|
||||
if !exists {
|
||||
src := &portainer.Source{
|
||||
Name: gittypes.RepoName(cfg.URL),
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: &gittypes.GitSource{
|
||||
URL: cfg.URL,
|
||||
Authentication: cfg.Authentication,
|
||||
TLSSkipVerify: cfg.TLSSkipVerify,
|
||||
},
|
||||
OwnerID: ownerId,
|
||||
Public: public,
|
||||
AdministratorsOnly: adminOnly,
|
||||
UserAccesses: users,
|
||||
TeamAccesses: teams,
|
||||
}
|
||||
|
||||
if err := m.sourceService.Tx(tx).Create(adminUserContext, src); err != nil {
|
||||
return fmt.Errorf("failed to create source for stack %d: %w", ls.ID, err)
|
||||
}
|
||||
srcID = src.ID
|
||||
newSrcID = src.ID
|
||||
} else {
|
||||
src, err := m.sourceService.Tx(tx).Read(adminUserContext, srcID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source %d for stack %d: %w", srcID, ls.ID, err)
|
||||
}
|
||||
|
||||
ApplyUACOnSourceUpdate_2_43_0(src, users, teams, public, adminOnly, ownerId)
|
||||
|
||||
if err := m.sourceService.Tx(tx).Update(adminUserContext, srcID, src); err != nil {
|
||||
return fmt.Errorf("failed to update source %d for stack %d: %w", srcID, ls.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
wf := &portainer.Workflow{
|
||||
Name: liveStack.Name,
|
||||
Artifacts: []portainer.Artifact{{
|
||||
StackID: portainer.StackID(ls.ID),
|
||||
Files: []portainer.ArtifactFile{{
|
||||
SourceID: srcID,
|
||||
Path: cfg.ConfigFilePath,
|
||||
Ref: cfg.ReferenceName,
|
||||
Hash: cfg.ConfigHash,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if err := m.workflowService.Tx(tx).Create(wf); err != nil {
|
||||
return fmt.Errorf("failed to create workflow for stack %d: %w", ls.ID, err)
|
||||
}
|
||||
|
||||
liveStack.WorkflowID = wf.ID
|
||||
liveStack.GitConfig = nil
|
||||
|
||||
return m.stackService.Tx(tx).Update(portainer.StackID(ls.ID), liveStack)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to migrate stack %d: %w", ls.ID, err)
|
||||
}
|
||||
|
||||
if newSrcID != 0 {
|
||||
sourcesByKey[key] = newSrcID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateCustomTemplateGitConfigToSources_2_43_0() error {
|
||||
log.Info().Msg("migrating git-backed custom templates to Source records")
|
||||
|
||||
templates, err := m.customTemplateService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
adminUserContext := source.InsecureNewAdminContext()
|
||||
existingSources, err := m.sourceService.ReadAll(adminUserContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourcesByKey := make(map[sourceDedupeKey]portainer.SourceID, len(existingSources))
|
||||
for _, src := range existingSources {
|
||||
if src.Git != nil {
|
||||
sourcesByKey[gitSourceKey(&gittypes.RepoConfig{URL: src.Git.URL, Authentication: src.Git.Authentication})] = src.ID
|
||||
}
|
||||
}
|
||||
|
||||
for i := range templates {
|
||||
t := &templates[i]
|
||||
if t.GitConfig == nil || t.Artifact != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := &gittypes.GitSource{
|
||||
URL: gittypes.SanitizeURL(t.GitConfig.URL),
|
||||
Authentication: t.GitConfig.Authentication,
|
||||
TLSSkipVerify: t.GitConfig.TLSSkipVerify,
|
||||
}
|
||||
|
||||
key := gitSourceKey(&gittypes.RepoConfig{URL: cfg.URL, Authentication: cfg.Authentication})
|
||||
|
||||
var newSrcID portainer.SourceID
|
||||
|
||||
if err := m.stackService.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
users, teams, public, adminOnly, ownerId := GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(t.ResourceControl,
|
||||
func() (portainer.UserID, portainer.UserRole, error) {
|
||||
user, err := m.userService.Tx(tx).Read(t.CreatedByUserID)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return user.ID, user.Role, nil
|
||||
},
|
||||
func(userId portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||
return m.teamMembershipService.Tx(tx).TeamMembershipsByUserID(userId)
|
||||
},
|
||||
)
|
||||
|
||||
srcID, exists := sourcesByKey[key]
|
||||
|
||||
if !exists {
|
||||
src := &portainer.Source{
|
||||
Name: gittypes.RepoName(cfg.URL),
|
||||
Type: portainer.SourceTypeGit,
|
||||
Git: cfg,
|
||||
OwnerID: ownerId,
|
||||
Public: public,
|
||||
AdministratorsOnly: adminOnly,
|
||||
UserAccesses: users,
|
||||
TeamAccesses: teams,
|
||||
}
|
||||
if err := m.sourceService.Tx(tx).Create(adminUserContext, src); err != nil {
|
||||
return fmt.Errorf("failed to create source for custom template %d: %w", t.ID, err)
|
||||
}
|
||||
srcID = src.ID
|
||||
newSrcID = src.ID
|
||||
} else {
|
||||
src, err := m.sourceService.Tx(tx).Read(adminUserContext, srcID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read source %d for custom template %d: %w", srcID, t.ID, err)
|
||||
}
|
||||
|
||||
ApplyUACOnSourceUpdate_2_43_0(src, users, teams, public, adminOnly, ownerId)
|
||||
|
||||
if err := m.sourceService.Tx(tx).Update(adminUserContext, srcID, src); err != nil {
|
||||
return fmt.Errorf("failed to update source %d for custom template %d: %w", srcID, t.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Artifact = &portainer.Artifact{
|
||||
Files: []portainer.ArtifactFile{{
|
||||
SourceID: srcID,
|
||||
Path: t.GitConfig.ConfigFilePath,
|
||||
Ref: t.GitConfig.ReferenceName,
|
||||
Hash: t.GitConfig.ConfigHash,
|
||||
}},
|
||||
}
|
||||
t.GitConfig = nil
|
||||
|
||||
return m.customTemplateService.Tx(tx).Update(t.ID, t)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to migrate custom template %d: %w", t.ID, err)
|
||||
}
|
||||
|
||||
if newSrcID != 0 {
|
||||
sourcesByKey[key] = newSrcID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices/customtemplate"
|
||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"github.com/portainer/portainer/api/dataservices/stack"
|
||||
"github.com/portainer/portainer/api/dataservices/teammembership"
|
||||
"github.com/portainer/portainer/api/dataservices/user"
|
||||
"github.com/portainer/portainer/api/dataservices/workflow"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO: generate tests for UAC migrations
|
||||
|
||||
var adminUserContext = source.InsecureNewAdminContext()
|
||||
|
||||
func TestMigrateGitConfigToSources_2_43_0_GitStackMigrated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
})
|
||||
|
||||
gitStack := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "git-stack",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
ReferenceName: "refs/heads/main",
|
||||
ConfigHash: "abc123",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(gitStack.ID), gitStack)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
migrated, err := stackSvc.Read(gitStack.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, migrated.WorkflowID)
|
||||
require.Nil(t, migrated.GitConfig)
|
||||
|
||||
wf, err := workflowSvc.Read(migrated.WorkflowID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, wf.Artifacts, 1)
|
||||
require.Len(t, wf.Artifacts[0].Files, 1)
|
||||
|
||||
src, err := sourceSvc.Read(adminUserContext, wf.Artifacts[0].Files[0].SourceID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||
require.Equal(t, gitStack.GitConfig.URL, src.Git.URL)
|
||||
require.Equal(t, gitStack.GitConfig.ReferenceName, wf.Artifacts[0].Files[0].Ref)
|
||||
}
|
||||
|
||||
func TestMigrateGitConfigToSources_2_43_0_NonGitStackUntouched(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
})
|
||||
|
||||
plainStack := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "plain-stack",
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(plainStack.ID), plainStack)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := stackSvc.Read(plainStack.ID)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, result.WorkflowID)
|
||||
require.Nil(t, result.GitConfig)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, sources)
|
||||
|
||||
workflows, err := workflowSvc.ReadAll()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, workflows)
|
||||
}
|
||||
|
||||
func TestMigrateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
})
|
||||
|
||||
sharedURL := "https://github.com/example/shared-repo"
|
||||
|
||||
stack1 := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "stack-a",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: sharedURL,
|
||||
ReferenceName: "refs/heads/main",
|
||||
},
|
||||
}
|
||||
stack2 := &portainer.Stack{
|
||||
ID: 2,
|
||||
Name: "stack-b",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: sharedURL,
|
||||
ReferenceName: "refs/heads/develop",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(stack1.ID), stack1)
|
||||
require.NoError(t, err)
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(stack2.ID), stack2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1, "two stacks with the same URL must share one Source")
|
||||
|
||||
workflows, err := workflowSvc.ReadAll()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workflows, 2, "each stack must get its own Workflow")
|
||||
|
||||
sharedSourceID := sources[0].ID
|
||||
for _, wf := range workflows {
|
||||
require.Len(t, wf.Artifacts, 1)
|
||||
require.Len(t, wf.Artifacts[0].Files, 1)
|
||||
require.Equal(t, sharedSourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateGitConfigToSources_2_43_0_DotGitSuffixDeduped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
})
|
||||
|
||||
// One stack uses the .git suffix, the other does not. Both refer to the
|
||||
// same repository and must share a single Source record after migration.
|
||||
stack1 := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "stack-a",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/shared-repo.git",
|
||||
ReferenceName: "refs/heads/main",
|
||||
},
|
||||
}
|
||||
stack2 := &portainer.Stack{
|
||||
ID: 2,
|
||||
Name: "stack-b",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/shared-repo",
|
||||
ReferenceName: "refs/heads/develop",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(stack1.ID), stack1)
|
||||
require.NoError(t, err)
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(stack2.ID), stack2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1, "stacks whose URLs differ only in .git suffix must share one Source")
|
||||
|
||||
workflows, err := workflowSvc.ReadAll()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workflows, 2, "each stack must get its own Workflow")
|
||||
|
||||
sharedSourceID := sources[0].ID
|
||||
for _, wf := range workflows {
|
||||
require.Len(t, wf.Artifacts, 1)
|
||||
require.Len(t, wf.Artifacts[0].Files, 1)
|
||||
require.Equal(t, sharedSourceID, wf.Artifacts[0].Files[0].SourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateGitConfigToSources_2_43_0_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
})
|
||||
|
||||
gitStack := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "git-stack",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(gitStack.ID), gitStack)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second run must not create duplicate Source/Workflow records
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1)
|
||||
|
||||
workflows, err := workflowSvc.ReadAll()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workflows, 1)
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_GitTemplateMigrated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
tmpl := &portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
ReferenceName: "refs/heads/main",
|
||||
ConfigFilePath: "docker-compose.yml",
|
||||
ConfigHash: "abc123",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
migrated, err := customTemplateSvc.Read(tmpl.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, migrated.Artifact)
|
||||
require.Nil(t, migrated.GitConfig)
|
||||
require.Len(t, migrated.Artifact.Files, 1)
|
||||
require.Equal(t, "refs/heads/main", migrated.Artifact.Files[0].Ref)
|
||||
require.Equal(t, "docker-compose.yml", migrated.Artifact.Files[0].Path)
|
||||
require.Equal(t, "abc123", migrated.Artifact.Files[0].Hash)
|
||||
|
||||
src, err := sourceSvc.Read(adminUserContext, migrated.Artifact.Files[0].SourceID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, portainer.SourceTypeGit, src.Type)
|
||||
require.Equal(t, "https://github.com/example/repo", src.Git.URL)
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_NonGitTemplateUntouched(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
tmpl := &portainer.CustomTemplate{ID: 1, Title: "plain-template"}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := customTemplateSvc.Read(tmpl.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, result.Artifact)
|
||||
require.Nil(t, result.GitConfig)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, sources)
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_AlreadyMigratedSkipped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
// Template already has Artifact set (already migrated)
|
||||
srcID := portainer.SourceID(99)
|
||||
tmpl := &portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
},
|
||||
Artifact: &portainer.Artifact{
|
||||
Files: []portainer.ArtifactFile{{SourceID: srcID}},
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, sources, "no new sources should be created for already-migrated templates")
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_DuplicateSourcesDeduped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
sharedURL := "https://github.com/example/shared-repo"
|
||||
|
||||
tmpl1 := &portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
Title: "template-a",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: sharedURL,
|
||||
ReferenceName: "refs/heads/main",
|
||||
},
|
||||
}
|
||||
tmpl2 := &portainer.CustomTemplate{
|
||||
ID: 2,
|
||||
Title: "template-b",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: sharedURL,
|
||||
ReferenceName: "refs/heads/develop",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl1.ID), tmpl1)
|
||||
require.NoError(t, err)
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl2.ID), tmpl2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1, "two templates with the same URL must share one Source")
|
||||
|
||||
sharedSrcID := sources[0].ID
|
||||
|
||||
migrated1, err := customTemplateSvc.Read(tmpl1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, migrated1.Artifact)
|
||||
require.Equal(t, sharedSrcID, migrated1.Artifact.Files[0].SourceID)
|
||||
|
||||
migrated2, err := customTemplateSvc.Read(tmpl2.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, migrated2.Artifact)
|
||||
require.Equal(t, sharedSrcID, migrated2.Artifact.Files[0].SourceID)
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_DotGitSuffixDeduped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
// One template uses the .git suffix, the other does not. Both refer to the
|
||||
// same repository and must share a single Source record after migration.
|
||||
tmpl1 := &portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
Title: "template-a",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/shared-repo.git",
|
||||
ReferenceName: "refs/heads/main",
|
||||
},
|
||||
}
|
||||
tmpl2 := &portainer.CustomTemplate{
|
||||
ID: 2,
|
||||
Title: "template-b",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/shared-repo",
|
||||
ReferenceName: "refs/heads/develop",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl1.ID), tmpl1)
|
||||
require.NoError(t, err)
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl2.ID), tmpl2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1, "templates whose URLs differ only in .git suffix must share one Source")
|
||||
|
||||
sharedSrcID := sources[0].ID
|
||||
|
||||
migrated1, err := customTemplateSvc.Read(tmpl1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, migrated1.Artifact)
|
||||
require.Equal(t, sharedSrcID, migrated1.Artifact.Files[0].SourceID)
|
||||
|
||||
migrated2, err := customTemplateSvc.Read(tmpl2.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, migrated2.Artifact)
|
||||
require.Equal(t, sharedSrcID, migrated2.Artifact.Files[0].SourceID)
|
||||
}
|
||||
|
||||
// TestMigrateGitConfigToSources_2_43_0_StandardUserStackPreservesAccess is the regression
|
||||
// test for the bug where stacks owned by standard users became inaccessible after upgrading
|
||||
// to 2.43. ResourceControls are stored in a separate bucket and are never embedded in the
|
||||
// stack record, so the migration must look them up explicitly.
|
||||
func TestMigrateGitConfigToSources_2_43_0_StandardUserStackPreservesAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
workflowSvc, err := workflow.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
rcSvc, err := resourcecontrol.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
userSvc, err := user.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
teamMembershipSvc, err := teammembership.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
WorkflowService: workflowSvc,
|
||||
ResourceControlService: rcSvc,
|
||||
UserService: userSvc,
|
||||
TeamMembershipService: teamMembershipSvc,
|
||||
})
|
||||
|
||||
standardUser := &portainer.User{
|
||||
Username: "standarduser",
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
err = userSvc.Create(standardUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
const endpointID portainer.EndpointID = 1
|
||||
|
||||
gitStack := &portainer.Stack{
|
||||
ID: 1,
|
||||
Name: "git-stack",
|
||||
EndpointID: endpointID,
|
||||
CreatedBy: "standarduser",
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
ReferenceName: "refs/heads/main",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(stack.BucketName, int(gitStack.ID), gitStack)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ResourceControls are stored separately from stacks; the stack record never embeds one.
|
||||
// The migration must look up the RC by resource ID to avoid defaulting to adminOnly=true.
|
||||
rc := &portainer.ResourceControl{
|
||||
ResourceID: fmt.Sprintf("%d_%s", endpointID, gitStack.Name),
|
||||
Type: portainer.StackResourceControl,
|
||||
UserAccesses: []portainer.UserResourceAccess{
|
||||
{UserID: standardUser.ID, AccessLevel: portainer.ReadWriteAccessLevel},
|
||||
},
|
||||
AdministratorsOnly: false,
|
||||
Public: false,
|
||||
}
|
||||
err = rcSvc.Create(rc)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
migrated, err := stackSvc.Read(gitStack.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, migrated.WorkflowID)
|
||||
|
||||
wf, err := workflowSvc.Read(migrated.WorkflowID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, wf.Artifacts, 1)
|
||||
require.Len(t, wf.Artifacts[0].Files, 1)
|
||||
|
||||
srcID := wf.Artifacts[0].Files[0].SourceID
|
||||
src, err := sourceSvc.Read(adminUserContext, srcID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, src.AdministratorsOnly, "source must not be admin-only after migrating a standard user's stack")
|
||||
require.Contains(t, src.UserAccesses, standardUser.ID)
|
||||
|
||||
// The standard user must be able to read the source through the normal access filter
|
||||
userCtx := source.NewUserContext(standardUser, nil)
|
||||
userSrc, err := sourceSvc.Read(userCtx, srcID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, srcID, userSrc.ID)
|
||||
}
|
||||
|
||||
func TestMigrateCustomTemplateGitConfigToSources_2_43_0_Idempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
conn := &boltdb.DbConnection{Path: t.TempDir()}
|
||||
err := conn.Open()
|
||||
require.NoError(t, err)
|
||||
defer logs.CloseAndLogErr(conn)
|
||||
|
||||
stackSvc, err := stack.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
sourceSvc, err := source.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
customTemplateSvc, err := customtemplate.NewService(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := NewMigrator(&MigratorParameters{
|
||||
StackService: stackSvc,
|
||||
SourceService: sourceSvc,
|
||||
CustomTemplateService: customTemplateSvc,
|
||||
})
|
||||
|
||||
tmpl := &portainer.CustomTemplate{
|
||||
ID: 1,
|
||||
GitConfig: &gittypes.RepoConfig{
|
||||
URL: "https://github.com/example/repo",
|
||||
},
|
||||
}
|
||||
err = conn.CreateObjectWithId(customtemplate.BucketName, int(tmpl.ID), tmpl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second run must not create duplicate Source records
|
||||
err = m.migrateCustomTemplateGitConfigToSources_2_43_0()
|
||||
require.NoError(t, err)
|
||||
|
||||
sources, err := sourceSvc.ReadAll(adminUserContext)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sources, 1)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// DB accesses enforcement are trying to restrict accesses as much as possible
|
||||
// but because accesses are applied sequentially, we want the accesses to be more open on migration
|
||||
// so that users retain their accesses
|
||||
func ApplyUACOnSourceUpdate_2_43_0(source *portainer.Source,
|
||||
users []portainer.UserID, teams []portainer.TeamID,
|
||||
public bool, adminOnly bool,
|
||||
ownerId portainer.UserID,
|
||||
) {
|
||||
// sources already public should remain public
|
||||
// OR
|
||||
// the resource using this source is public, so the source should be public
|
||||
if source.Public || public {
|
||||
source.Public = true
|
||||
source.AdministratorsOnly = false
|
||||
source.UserAccesses = []portainer.UserID{}
|
||||
source.TeamAccesses = []portainer.TeamID{}
|
||||
return
|
||||
}
|
||||
|
||||
// add users and teams to source accesses only if the incoming resource is not admninonly
|
||||
// to avoid saving leftover user/teams from adminonly resources
|
||||
if !adminOnly {
|
||||
source.UserAccesses = slicesx.Unique(append(source.UserAccesses, users...))
|
||||
source.TeamAccesses = slicesx.Unique(append(source.TeamAccesses, teams...))
|
||||
}
|
||||
|
||||
// regardless of the incoming resource's ResourceControl values (func params)
|
||||
// no accesses means adminonly source not owned by anyone
|
||||
// we don't want users to own sources they don't have access to
|
||||
// neither we want to default them to public
|
||||
// all in all as we are doing an update it's probably redundant, but just in case...
|
||||
if len(source.UserAccesses) == 0 && len(source.TeamAccesses) == 0 {
|
||||
source.AdministratorsOnly = true
|
||||
source.OwnerID = 0
|
||||
return
|
||||
}
|
||||
|
||||
// if owner of the incoming resource (ownerid) is the only one with access, we give the ownership to the user.
|
||||
// The source could previously be adminonly so we change that as well as we want the most open situation
|
||||
if len(source.UserAccesses) == 1 && len(source.TeamAccesses) == 0 && ownerId == source.UserAccesses[0] {
|
||||
source.OwnerID = ownerId
|
||||
source.AdministratorsOnly = false
|
||||
return
|
||||
}
|
||||
|
||||
// Anything else will have multiple accesses (multiple teams or users), from multiple resources (source update flow)
|
||||
// So we remove the ownership of the source in case it existed
|
||||
// Scenario:
|
||||
// - source created for resource owned by Bob
|
||||
// - now we try to update with the RC from an admin-owned resource, shared to other users/teams
|
||||
// - we don't want Bob to own the source anymore
|
||||
source.OwnerID = 0
|
||||
|
||||
}
|
||||
|
||||
func GetValuesForUsersFromResourceOwnershipAndAccesses_2_43_0(
|
||||
rc *portainer.ResourceControl,
|
||||
getCreator func() (portainer.UserID, portainer.UserRole, error),
|
||||
getCreatorMemberships func(portainer.UserID) ([]portainer.TeamMembership, error),
|
||||
) (
|
||||
users []portainer.UserID, teams []portainer.TeamID,
|
||||
public bool, adminOnly bool,
|
||||
ownerId portainer.UserID,
|
||||
) {
|
||||
users = []portainer.UserID{}
|
||||
teams = []portainer.TeamID{}
|
||||
public = false
|
||||
adminOnly = true
|
||||
|
||||
if rc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
adminOnly = rc.AdministratorsOnly
|
||||
public = rc.Public
|
||||
|
||||
if adminOnly || public {
|
||||
return
|
||||
}
|
||||
|
||||
// only transfer users/teams when the stack is not admin nor public
|
||||
// this allows avoiding transfering access of sources to users/teams that don't have real access to the stack
|
||||
// but that may have had their accesses retained in DB
|
||||
|
||||
users = slicesx.Map(rc.UserAccesses, func(ura portainer.UserResourceAccess) portainer.UserID { return ura.UserID })
|
||||
teams = slicesx.Map(rc.TeamAccesses, func(tra portainer.TeamResourceAccess) portainer.TeamID { return tra.TeamID })
|
||||
|
||||
userId, userRole, err := getCreator()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to read user when migrating to source")
|
||||
return
|
||||
}
|
||||
|
||||
// we don't want to save the ownerid if the user is admin
|
||||
// this avoids admins taking ownership of a new source
|
||||
if userRole == portainer.AdministratorRole {
|
||||
return
|
||||
}
|
||||
|
||||
// We also don't want to get the ownerid if the user doesn't have access to the resource anymore
|
||||
userTeams, err := getCreatorMemberships(userId)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("failed to read user %d teams when migrating source", userId)
|
||||
return
|
||||
}
|
||||
|
||||
teamIds := slicesx.Map(userTeams, func(membership portainer.TeamMembership) portainer.TeamID { return membership.TeamID })
|
||||
if authorization.UserCanAccessResource(userId, teamIds, rc) {
|
||||
ownerId = userId
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return errors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
func GetFunctionName(i any) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
version, err := m.versionService.Version()
|
||||
if err != nil {
|
||||
return migrationError(err, "get version service")
|
||||
}
|
||||
|
||||
schemaVersion, err := semver.NewVersion(version.SchemaVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "invalid db schema version")
|
||||
}
|
||||
|
||||
newMigratorCount := 0
|
||||
apiVersion := semver.MustParse(portainer.APIVersion)
|
||||
if schemaVersion.Equal(apiVersion) {
|
||||
// detect and run migrations when the versions are the same.
|
||||
// e.g. development builds
|
||||
latestMigrations := m.LatestMigrations()
|
||||
if latestMigrations.Version.Equal(schemaVersion) &&
|
||||
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
||||
if err := runMigrations(latestMigrations.MigrationFuncs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newMigratorCount = len(latestMigrations.MigrationFuncs)
|
||||
}
|
||||
} else {
|
||||
// regular path when major/minor/patch versions differ
|
||||
for _, migration := range m.migrations {
|
||||
if schemaVersion.LessThan(migration.Version) {
|
||||
log.Info().Msgf("migrating data to %s", migration.Version.String())
|
||||
|
||||
if err := runMigrations(migration.MigrationFuncs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if apiVersion.Equal(migration.Version) {
|
||||
newMigratorCount = len(migration.MigrationFuncs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.Always(); err != nil {
|
||||
return migrationError(err, "Always migrations returned error")
|
||||
}
|
||||
|
||||
version.SchemaVersion = portainer.APIVersion
|
||||
version.MigratorCount = newMigratorCount
|
||||
|
||||
if err := m.versionService.UpdateVersion(version); err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
|
||||
log.Info().Msgf("db migrated to %s", portainer.APIVersion)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMigrations(migrationFuncs []func() error) error {
|
||||
for _, migrationFunc := range migrationFuncs {
|
||||
err := migrationFunc()
|
||||
if err != nil {
|
||||
return migrationError(err, GetFunctionName(migrationFunc))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) NeedsMigration() bool {
|
||||
// we need to migrate if anything changes with the version in the DB vs what our software version is.
|
||||
// If the version matches, then it's all down to the number of migration funcs we have for the current version
|
||||
// i.e. the MigratorCount
|
||||
|
||||
// In this particular instance we should log a fatal error
|
||||
if m.CurrentDBEdition() != portainer.PortainerCE {
|
||||
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://docs.portainer.io/faqs/upgrading/can-i-downgrade-from-portainer-business-to-portainer-ce")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if m.CurrentSemanticDBVersion().LessThan(semver.MustParse(portainer.APIVersion)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we have any migrations for the current version
|
||||
latestMigrations := m.LatestMigrations()
|
||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||
if m.currentDBVersion.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// One remaining possibility if we get here. If our migrator count > 0 and we have no migration funcs
|
||||
// for the current version (i.e. they were deleted during development). Then we we need to migrate.
|
||||
// This is to reset the migrator count back to 0
|
||||
if m.currentDBVersion.MigratorCount > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/chisel/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDockerDesktopExtensionSetting() error {
|
||||
log.Info().Msg("updating docker desktop extension flag in settings")
|
||||
|
||||
isDDExtension := false
|
||||
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
||||
isDDExtension = true
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.IsDockerDesktopExtension = isDDExtension
|
||||
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
|
||||
var serverInfo *portainer.TunnelServerInfo
|
||||
|
||||
serverInfo, err := m.TunnelServerService.Info()
|
||||
if err != nil {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
log.Info().Msg("ServerInfo object not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to read ServerInfo from DB")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if serverInfo.PrivateKeySeed != "" {
|
||||
key, err := crypto.GenerateGo119CompatibleKey(serverInfo.PrivateKeySeed)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to read ServerInfo from DB")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.fileService.StoreChiselPrivateKey(key); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to save Chisel private key to disk")
|
||||
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info().Msg("PrivateKeySeed is blank")
|
||||
}
|
||||
|
||||
serverInfo.PrivateKeySeed = ""
|
||||
if err := m.TunnelServerService.UpdateInfo(serverInfo); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("Failed to clean private key seed in DB")
|
||||
} else {
|
||||
log.Info().Msg("Success to migrate private key seed to private key file")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
||||
log.Info().Msg("update edge stack status to have deployment steps")
|
||||
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for environmentID, environmentStatus := range edgeStack.Status {
|
||||
// Skip if status is already updated
|
||||
if len(environmentStatus.Status) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if environmentStatus.Details == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
statusArray := []portainer.EdgeStackDeploymentStatus{}
|
||||
if environmentStatus.Details.Pending {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusPending,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Acknowledged {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusAcknowledged,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Error {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusError,
|
||||
Error: environmentStatus.Error,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Ok {
|
||||
statusArray = append(statusArray,
|
||||
portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusDeploymentReceived,
|
||||
Time: time.Now().Unix(),
|
||||
},
|
||||
portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRunning,
|
||||
Time: time.Now().Unix(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if environmentStatus.Details.ImagesPulled {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusImagesPulled,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
if environmentStatus.Details.Remove {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
Type: portainer.EdgeStackStatusRemoving,
|
||||
Time: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
environmentStatus.Status = statusArray
|
||||
|
||||
edgeStack.Status[environmentID] = environmentStatus
|
||||
}
|
||||
|
||||
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// updateAppTemplatesVersionForDB110 changes the templates URL to be empty if it was never changed
|
||||
// from the default value (version 2.0 URL)
|
||||
func (migrator *Migrator) updateAppTemplatesVersionForDB110() error {
|
||||
log.Info().Msg("updating app templates url to v3.0")
|
||||
|
||||
version2URL := "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
|
||||
settings, err := migrator.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.TemplatesURL == version2URL || settings.TemplatesURL == portainer.DefaultTemplatesURL {
|
||||
settings.TemplatesURL = ""
|
||||
}
|
||||
|
||||
return migrator.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
// In PortainerCE the resource overcommit option should always be true across all endpoints
|
||||
func (migrator *Migrator) updateResourceOverCommitToDB110() error {
|
||||
log.Info().Msg("updating resource overcommit setting to true")
|
||||
|
||||
endpoints, err := migrator.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.KubernetesLocalEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
||||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
endpoint.Kubernetes.Configuration.EnableResourceOverCommit = true
|
||||
|
||||
err = migrator.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
|
||||
log.Info().Msg("cleaning up pending actions for deleted endpoints")
|
||||
|
||||
pendingActions, err := migrator.pendingActionsService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints := make(map[portainer.EndpointID]struct{})
|
||||
for _, action := range pendingActions {
|
||||
endpoints[action.EndpointID] = struct{}{}
|
||||
}
|
||||
|
||||
for endpointId := range endpoints {
|
||||
_, err := migrator.endpointService.Endpoint(endpointId)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/segmentio/encoding/json"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (migrator *Migrator) migratePendingActionsDataForDB130() error {
|
||||
log.Info().Msg("Migrating pending actions data")
|
||||
|
||||
pendingActions, err := migrator.pendingActionsService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pa := range pendingActions {
|
||||
actionData, err := json.Marshal(pa.ActionData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pa.ActionData = string(actionData)
|
||||
|
||||
// Update the pending action
|
||||
err = migrator.pendingActionsService.Update(pa.ID, &pa)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// migrateRegistryAccessSASecrets_2_40_0 marks Kubernetes endpoints that have
|
||||
// registry access configured so that imagePullSecrets can be added to their
|
||||
// default ServiceAccounts during the post-init migration phase (when cluster
|
||||
// access is available).
|
||||
func (m *Migrator) migrateRegistryAccessSASecrets_2_40_0() error {
|
||||
log.Info().Msg("migrating registry access service account secrets")
|
||||
|
||||
registries, err := m.registryService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Collect the IDs of endpoints that have at least one registry with
|
||||
// non-empty namespace access - these need the SA imagePullSecrets migration.
|
||||
needsMigration := make(map[portainer.EndpointID]bool)
|
||||
for _, registry := range registries {
|
||||
for endpointID, access := range registry.RegistryAccesses {
|
||||
if len(access.Namespaces) > 0 {
|
||||
needsMigration[endpointID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
endpoint := &endpoints[i]
|
||||
|
||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !needsMigration[endpoint.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint.PostInitMigrations.MigrateRegistrySASecrets = true
|
||||
if err := m.endpointService.UpdateEndpoint(endpoint.ID, endpoint); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Int("endpointID", int(endpoint.ID)).
|
||||
Msg("failed to set registry SA secret migration flag for endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateUsersToDBVersion18() error {
|
||||
log.Info().Msg("updating users")
|
||||
|
||||
legacyUsers, err := m.userService.ReadAll()
|
||||
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.Update(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsToDBVersion18() error {
|
||||
log.Info().Msg("updating endpoints")
|
||||
|
||||
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 {
|
||||
log.Info().Msg("updating endpoint groups")
|
||||
|
||||
legacyEndpointGroups, err := m.endpointGroupService.ReadAll()
|
||||
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.Update(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRegistriesToDBVersion18() error {
|
||||
log.Info().Msg("updating registries")
|
||||
|
||||
legacyRegistries, err := m.registryService.ReadAll()
|
||||
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.Update(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion19() error {
|
||||
log.Info().Msg("updating settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.EdgeAgentCheckinInterval == 0 {
|
||||
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||
}
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const scheduleScriptExecutionJobType = 1
|
||||
|
||||
func (m *Migrator) updateUsersToDBVersion20() error {
|
||||
log.Info().Msg("updating user authentication")
|
||||
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion20() error {
|
||||
log.Info().Msg("updating settings")
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowVolumeBrowserForRegularUsers = false
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSchedulesToDBVersion20() error {
|
||||
log.Info().Msg("updating schedules")
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
log.Info().Msg("updating resource controls")
|
||||
|
||||
legacyResourceControls, err := m.resourceControlService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
|
||||
if err := m.resourceControlService.Update(resourceControl.ID, &resourceControl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
log.Info().Msg("updating users and roles")
|
||||
|
||||
legacyUsers, err := m.userService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
|
||||
|
||||
if err := m.userService.Update(user.ID, &user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointAdministratorRole, err := m.roleService.Read(portainer.RoleID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointAdministratorRole.Priority = 1
|
||||
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||
|
||||
if err := m.roleService.Update(endpointAdministratorRole.ID, endpointAdministratorRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helpDeskRole, err := m.roleService.Read(portainer.RoleID(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(helpDeskRole.ID, helpDeskRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
standardUserRole, err := m.roleService.Read(portainer.RoleID(3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(standardUserRole.ID, standardUserRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readOnlyUserRole, err := m.roleService.Read(portainer.RoleID(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(readOnlyUserRole.ID, readOnlyUserRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
log.Info().Msg("updating tags")
|
||||
|
||||
tags, err := m.tagService.ReadAll()
|
||||
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.Update(tag.ID, &tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
|
||||
log.Info().Msg("updating endpoints and endpoint groups")
|
||||
|
||||
tags, err := m.tagService.ReadAll()
|
||||
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.ReadAll()
|
||||
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.Update(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, tag := range tagsNameMap {
|
||||
err = m.tagService.Update(tag.ID, &tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
log.Info().Msg("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 {
|
||||
log.Info().Msg("updating stacks")
|
||||
|
||||
stacks, err := m.stackService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx := range stacks {
|
||||
stack := &stacks[idx]
|
||||
stack.Status = portainer.StackStatusActive
|
||||
|
||||
if err := m.stackService.Update(stack.ID, stack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB25() error {
|
||||
log.Info().Msg("updating settings")
|
||||
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// to keep the same migration functionality as before 2.20.0, we need to set the templates URL to v2
|
||||
version2URL := "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
if legacySettings.TemplatesURL == "" {
|
||||
legacySettings.TemplatesURL = version2URL
|
||||
}
|
||||
|
||||
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
|
||||
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
log.Info().Msg("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
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
log.Info().Msg("updating stack resource controls")
|
||||
|
||||
resourceControls, err := m.resourceControlService.ReadAll()
|
||||
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 dataservices.IsErrObjectNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
resource.ResourceID = stackutils.ResourceControlID(stack.EndpointID, stack.Name)
|
||||
|
||||
err = m.resourceControlService.Update(resource.ID, &resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB30() error {
|
||||
log.Info().Msg("updating legacy settings")
|
||||
|
||||
return m.MigrateSettingsToDB30()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB32() error {
|
||||
err := m.updateRegistriesToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateDockerhubToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateVolumeResourceControlToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.kubeconfigExpiryToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.helmRepositoryURLToDB32(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateRegistriesToDB32() error {
|
||||
log.Info().Msg("updating registries")
|
||||
|
||||
registries, err := m.registryService.ReadAll()
|
||||
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{},
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.registryService.Update(registry.ID, ®istry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateDockerhubToDB32() error {
|
||||
log.Info().Msg("updating dockerhub")
|
||||
|
||||
dockerhub, err := m.dockerhubService.DockerHub()
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
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.ReadAll()
|
||||
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
|
||||
// delete subsequent duplicates
|
||||
} else if err := m.registryService.Delete(r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Info().Msg("updating resource controls")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environments: %w", err)
|
||||
}
|
||||
|
||||
resourceControls, err := m.resourceControlService.ReadAll()
|
||||
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.Debug().Msg("no snapshot found")
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot := endpoint.Snapshots[totalSnapshots-1]
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed fetching environment docker id")
|
||||
continue
|
||||
}
|
||||
|
||||
volumesData := snapshot.SnapshotRaw.Volumes
|
||||
if volumesData.Volumes == nil {
|
||||
log.Debug().Msg("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.Update(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed updating resource control %d: %w", resourceControl.ID, err)
|
||||
}
|
||||
} else {
|
||||
err := m.resourceControlService.Delete(resourceControl.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
|
||||
}
|
||||
|
||||
log.Debug().Str("resource_id", resourceControl.ResourceID).Msg("legacy resource control has been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findResourcesToUpdateForDB32(dockerID string, volumesData volume.ListResponse, toUpdate map[portainer.ResourceControlID]string, volumeResourceControls map[string]*portainer.ResourceControl) {
|
||||
volumes := volumesData.Volumes
|
||||
for _, volume := range volumes {
|
||||
volumeName := volume.Name
|
||||
createTime := volume.CreatedAt
|
||||
|
||||
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 {
|
||||
log.Info().Msg("updating kubeconfig expiry")
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.KubeconfigExpiry = portainer.DefaultKubeconfigExpiry
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func (m *Migrator) helmRepositoryURLToDB32() error {
|
||||
log.Info().Msg("setting default helm repository URL")
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.HelmRepositoryURL = portainer.DefaultHelmRepositoryURL
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB33() error {
|
||||
log.Info().Msg("updating settings")
|
||||
|
||||
return m.migrateSettingsToDB33()
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateSettingsToDB33() error {
|
||||
log.Info().Msg("setting default kubctl shell")
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Msg("setting default kubectl shell image")
|
||||
settings.KubectlShellImage = *m.flags.KubectlShellImage
|
||||
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
log.Info().Msg("updating stacks")
|
||||
|
||||
return MigrateStackEntryPoint(m.stackService)
|
||||
}
|
||||
|
||||
// MigrateStackEntryPoint exported for testing
|
||||
func MigrateStackEntryPoint(stackService dataservices.StackService) error {
|
||||
stacks, err := stackService.ReadAll()
|
||||
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.Update(stack.ID, stack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
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.
|
||||
return m.updateDockerhubToDB32()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB36() error {
|
||||
log.Info().Msg("updating user authorizations")
|
||||
|
||||
return m.migrateUsersToDB36()
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateUsersToDB36() error {
|
||||
log.Info().Msg("updating user authorizations")
|
||||
|
||||
users, err := m.userService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
currentAuthorizations := authorization.DefaultPortainerAuthorizations()
|
||||
currentAuthorizations[portainer.OperationPortainerUserListToken] = true
|
||||
currentAuthorizations[portainer.OperationPortainerUserCreateToken] = true
|
||||
currentAuthorizations[portainer.OperationPortainerUserRevokeToken] = true
|
||||
user.PortainerAuthorizations = currentAuthorizations
|
||||
err = m.userService.Update(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB40() error {
|
||||
return m.trustCurrentEdgeEndpointsDB40()
|
||||
}
|
||||
|
||||
func (m *Migrator) trustCurrentEdgeEndpointsDB40() error {
|
||||
log.Info().Msg("trusting current edge endpoints")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
endpoint.UserTrusted = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB50() error {
|
||||
return m.migratePasswordLengthSettings()
|
||||
}
|
||||
|
||||
func (m *Migrator) migratePasswordLengthSettings() error {
|
||||
log.Info().Msg("updating required password length")
|
||||
|
||||
s, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve settings")
|
||||
}
|
||||
|
||||
s.InternalAuthSettings.RequiredPasswordLength = 12
|
||||
return m.settingsService.UpdateSettings(s)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB60() error {
|
||||
return m.addGpuInputFieldDB60()
|
||||
}
|
||||
|
||||
func (m *Migrator) addGpuInputFieldDB60() error {
|
||||
log.Info().Msg("add gpu input field")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Gpus == nil {
|
||||
endpoint.Gpus = []portainer.Pair{}
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB70() error {
|
||||
log.Info().Msg("add IngressAvailabilityPerNamespace field")
|
||||
if err := m.updateIngressFieldsForEnvDB70(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
// copy snapshots to new object
|
||||
log.Info().Msg("moving snapshots from endpoint to new object")
|
||||
snapshot := portainer.Snapshot{EndpointID: endpoint.ID}
|
||||
|
||||
if len(endpoint.Snapshots) > 0 {
|
||||
snapshot.Docker = &endpoint.Snapshots[len(endpoint.Snapshots)-1]
|
||||
}
|
||||
|
||||
if len(endpoint.Kubernetes.Snapshots) > 0 {
|
||||
snapshot.Kubernetes = &endpoint.Kubernetes.Snapshots[len(endpoint.Kubernetes.Snapshots)-1]
|
||||
}
|
||||
|
||||
// save new object
|
||||
err = m.snapshotService.Create(&snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set to nil old fields
|
||||
log.Info().Msg("deleting snapshot from endpoint")
|
||||
endpoint.Snapshots = []portainer.DockerSnapshot{}
|
||||
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{}
|
||||
|
||||
// update endpoint
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateIngressFieldsForEnvDB70() error {
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true
|
||||
endpoint.Kubernetes.Configuration.AllowNoneIngressClass = false
|
||||
endpoint.PostInitMigrations.MigrateIngresses = true
|
||||
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB71() error {
|
||||
log.Info().Msg("removing orphaned snapshots")
|
||||
|
||||
snapshots, err := m.snapshotService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range snapshots {
|
||||
_, err := m.endpointService.Endpoint(s.EndpointID)
|
||||
if err == nil {
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("keeping snapshot")
|
||||
continue
|
||||
} else if !dataservices.IsErrObjectNotFound(err) {
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Err(err).Msg("database error")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("removing snapshot")
|
||||
|
||||
err = m.snapshotService.Delete(s.EndpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB80() error {
|
||||
if err := m.updateEdgeStackStatusForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateExistingEndpointsToNotDetectMetricsAPIForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateExistingEndpointsToNotDetectStorageAPIForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateExistingEndpointsToNotDetectMetricsAPIForDB80() error {
|
||||
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateExistingEndpointsToNotDetectStorageAPIForDB80() error {
|
||||
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
||||
log.Info().Msg("transfer type field to details field for edge stack status")
|
||||
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for endpointId, status := range edgeStack.Status {
|
||||
if status.Details == nil {
|
||||
status.Details = &portainer.EdgeStackStatusDetails{}
|
||||
}
|
||||
|
||||
switch status.Type {
|
||||
case portainer.EdgeStackStatusPending:
|
||||
status.Details.Pending = true
|
||||
case portainer.EdgeStackStatusDeploymentReceived:
|
||||
status.Details.Ok = true
|
||||
case portainer.EdgeStackStatusError:
|
||||
status.Details.Error = true
|
||||
case portainer.EdgeStackStatusAcknowledged:
|
||||
status.Details.Acknowledged = true
|
||||
case portainer.EdgeStackStatusRemoved:
|
||||
status.Details.Remove = true
|
||||
case portainer.EdgeStackStatusRemoteUpdateSuccess:
|
||||
status.Details.RemoteUpdateSuccess = true
|
||||
}
|
||||
|
||||
edgeStack.Status[endpointId] = status
|
||||
}
|
||||
|
||||
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB90() error {
|
||||
if err := m.updateUserThemeForDB90(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateEnableGpuManagementFeatures(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.updateEdgeStackStatusForDB90()
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB90() error {
|
||||
log.Info().Msg("clean up deleted endpoints from edge jobs")
|
||||
|
||||
edgeJobs, err := m.edgeJobService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeJob := range edgeJobs {
|
||||
for endpointId := range edgeJob.Endpoints {
|
||||
_, err := m.endpointService.Endpoint(endpointId)
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
delete(edgeJob.Endpoints, endpointId)
|
||||
|
||||
err = m.edgeJobService.Update(edgeJob.ID, &edgeJob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUserThemeForDB90() error {
|
||||
log.Info().Msg("updating existing user theme settings")
|
||||
|
||||
users, err := m.userService.ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
user := &users[i]
|
||||
if user.UserTheme != "" {
|
||||
user.ThemeSettings.Color = user.UserTheme
|
||||
}
|
||||
|
||||
if err := m.userService.Update(user.ID, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEnableGpuManagementFeatures() error {
|
||||
// get all environments
|
||||
environments, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, environment := range environments {
|
||||
if environment.Type == portainer.DockerEnvironment {
|
||||
// set the PostInitMigrations.MigrateGPUs to true on this environment to run the migration only on the 2.18 upgrade
|
||||
environment.PostInitMigrations.MigrateGPUs = true
|
||||
// if there's one or more gpu, set the EnableGpuManagement setting to true
|
||||
gpuList := environment.Gpus
|
||||
if len(gpuList) > 0 {
|
||||
environment.EnableGPUManagement = true
|
||||
}
|
||||
// update the environment
|
||||
if err := m.endpointService.UpdateEndpoint(environment.ID, &environment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"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/edgestackstatus"
|
||||
"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/pendingactions"
|
||||
"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/snapshot"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"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/tunnelserver"
|
||||
"github.com/portainer/portainer/api/dataservices/user"
|
||||
"github.com/portainer/portainer/api/dataservices/version"
|
||||
"github.com/portainer/portainer/api/dataservices/workflow"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
flags *portainer.CLIFlags
|
||||
currentDBVersion *models.Version
|
||||
migrations []Migrations
|
||||
|
||||
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
|
||||
snapshotService *snapshot.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
|
||||
edgeStackService *edgestack.Service
|
||||
edgeStackStatusService *edgestackstatus.Service
|
||||
edgeJobService *edgejob.Service
|
||||
edgeGroupService *edgegroup.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
pendingActionsService *pendingactions.Service
|
||||
customTemplateService *customtemplate.Service
|
||||
sourceService *source.Service
|
||||
workflowService *workflow.Service
|
||||
}
|
||||
|
||||
// MigratorParameters represents the required parameters to create a new Migrator instance.
|
||||
MigratorParameters struct {
|
||||
Flags *portainer.CLIFlags
|
||||
CurrentDBVersion *models.Version
|
||||
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
|
||||
SnapshotService *snapshot.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
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeStackStatusService *edgestackstatus.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
PendingActionsService *pendingactions.Service
|
||||
CustomTemplateService *customtemplate.Service
|
||||
SourceService *source.Service
|
||||
WorkflowService *workflow.Service
|
||||
}
|
||||
)
|
||||
|
||||
// NewMigrator creates a new Migrator.
|
||||
func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||
migrator := &Migrator{
|
||||
flags: parameters.Flags,
|
||||
currentDBVersion: parameters.CurrentDBVersion,
|
||||
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,
|
||||
snapshotService: parameters.SnapshotService,
|
||||
tagService: parameters.TagService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
stackService: parameters.StackService,
|
||||
userService: parameters.UserService,
|
||||
versionService: parameters.VersionService,
|
||||
fileService: parameters.FileService,
|
||||
authorizationService: parameters.AuthorizationService,
|
||||
dockerhubService: parameters.DockerhubService,
|
||||
edgeStackService: parameters.EdgeStackService,
|
||||
edgeStackStatusService: parameters.EdgeStackStatusService,
|
||||
edgeJobService: parameters.EdgeJobService,
|
||||
edgeGroupService: parameters.EdgeGroupService,
|
||||
TunnelServerService: parameters.TunnelServerService,
|
||||
pendingActionsService: parameters.PendingActionsService,
|
||||
customTemplateService: parameters.CustomTemplateService,
|
||||
sourceService: parameters.SourceService,
|
||||
workflowService: parameters.WorkflowService,
|
||||
}
|
||||
|
||||
migrator.initMigrations()
|
||||
|
||||
return migrator
|
||||
}
|
||||
|
||||
func (m *Migrator) CurrentDBVersion() string {
|
||||
return m.currentDBVersion.SchemaVersion
|
||||
}
|
||||
|
||||
func (m *Migrator) CurrentDBEdition() portainer.SoftwareEdition {
|
||||
return portainer.SoftwareEdition(m.currentDBVersion.Edition)
|
||||
}
|
||||
|
||||
func (m *Migrator) CurrentSemanticDBVersion() *semver.Version {
|
||||
v, err := semver.NewVersion(m.currentDBVersion.SchemaVersion)
|
||||
if err != nil {
|
||||
log.Fatal().Stack().Err(err).Msg("failed to parse current version")
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (m *Migrator) addMigrations(v string, funcs ...func() error) {
|
||||
m.migrations = append(m.migrations, Migrations{
|
||||
Version: semver.MustParse(v),
|
||||
MigrationFuncs: funcs,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Migrator) LatestMigrations() Migrations {
|
||||
return m.migrations[len(m.migrations)-1]
|
||||
}
|
||||
|
||||
func (m *Migrator) GetMigratorCountOfCurrentAPIVersion() int {
|
||||
migratorCount := 0
|
||||
latestMigrations := m.LatestMigrations()
|
||||
|
||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||
migratorCount = len(latestMigrations.MigrationFuncs)
|
||||
}
|
||||
|
||||
return migratorCount
|
||||
}
|
||||
|
||||
// !NOTE: Migration funtions should ideally be idempotent.
|
||||
// ! Which simply means the function can run over the same data many times but only transform it once.
|
||||
// ! In practice this really just means an extra check or two to ensure we're not destroying valid data.
|
||||
// ! This is not a hard rule though. Understand the limitations. A migration function may only run over
|
||||
// ! the data more than once if a new migration function is added and the version of your database schema is
|
||||
// ! the same. e.g. two developers working on the same version add two different functions for different things.
|
||||
// ! This increases the migration funcs count and so they all run again.
|
||||
|
||||
type Migrations struct {
|
||||
Version *semver.Version
|
||||
MigrationFuncs MigrationFuncs
|
||||
}
|
||||
|
||||
type MigrationFuncs []func() error
|
||||
|
||||
func (m *Migrator) initMigrations() {
|
||||
// !IMPORTANT: Do not be tempted to alter the order of these migrations.
|
||||
// ! Even though one of them looks out of order. Caused by history related
|
||||
// ! to maintaining two versions and releasing at different times
|
||||
|
||||
m.addMigrations("1.0.0", dbTooOldError) // default version found after migration
|
||||
|
||||
m.addMigrations("1.21",
|
||||
m.updateUsersToDBVersion18,
|
||||
m.updateEndpointsToDBVersion18,
|
||||
m.updateEndpointGroupsToDBVersion18,
|
||||
m.updateRegistriesToDBVersion18)
|
||||
|
||||
m.addMigrations("1.22", m.updateSettingsToDBVersion19)
|
||||
|
||||
m.addMigrations("1.22.1",
|
||||
m.updateUsersToDBVersion20,
|
||||
m.updateSettingsToDBVersion20,
|
||||
m.updateSchedulesToDBVersion20)
|
||||
|
||||
m.addMigrations("1.23",
|
||||
m.updateResourceControlsToDBVersion22,
|
||||
m.updateUsersAndRolesToDBVersion22)
|
||||
|
||||
m.addMigrations("1.24",
|
||||
m.updateTagsToDBVersion23,
|
||||
m.updateEndpointsAndEndpointGroupsToDBVersion23)
|
||||
|
||||
m.addMigrations("1.24.1", m.updateSettingsToDB24)
|
||||
|
||||
m.addMigrations("2.0",
|
||||
m.updateSettingsToDB25,
|
||||
m.updateStacksToDB24)
|
||||
|
||||
m.addMigrations("2.1", m.updateEndpointSettingsToDB25)
|
||||
m.addMigrations("2.2", m.updateStackResourceControlToDB27)
|
||||
m.addMigrations("2.6", m.migrateDBVersionToDB30)
|
||||
m.addMigrations("2.9", m.migrateDBVersionToDB32)
|
||||
m.addMigrations("2.9.2", m.migrateDBVersionToDB33)
|
||||
m.addMigrations("2.10.0", m.migrateDBVersionToDB34)
|
||||
m.addMigrations("2.9.3", m.migrateDBVersionToDB35)
|
||||
m.addMigrations("2.12", m.migrateDBVersionToDB36)
|
||||
m.addMigrations("2.13", m.migrateDBVersionToDB40)
|
||||
m.addMigrations("2.14", m.migrateDBVersionToDB50)
|
||||
m.addMigrations("2.15", m.migrateDBVersionToDB60)
|
||||
m.addMigrations("2.16", m.migrateDBVersionToDB70)
|
||||
m.addMigrations("2.16.1", m.migrateDBVersionToDB71)
|
||||
m.addMigrations("2.17", m.migrateDBVersionToDB80)
|
||||
m.addMigrations("2.18", m.migrateDBVersionToDB90)
|
||||
m.addMigrations("2.19",
|
||||
m.convertSeedToPrivateKeyForDB100,
|
||||
m.migrateDockerDesktopExtensionSetting,
|
||||
m.updateEdgeStackStatusForDB100,
|
||||
)
|
||||
m.addMigrations("2.20",
|
||||
m.updateAppTemplatesVersionForDB110,
|
||||
m.updateResourceOverCommitToDB110,
|
||||
)
|
||||
m.addMigrations("2.20.2",
|
||||
m.cleanPendingActionsForDeletedEndpointsForDB111,
|
||||
)
|
||||
m.addMigrations("2.22.0",
|
||||
m.migratePendingActionsDataForDB130,
|
||||
)
|
||||
|
||||
m.addMigrations("2.31.0", m.migrateEdgeStacksStatuses_2_31_0)
|
||||
|
||||
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
|
||||
|
||||
m.addMigrations("2.33.1", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
|
||||
|
||||
m.addMigrations("2.40.0", m.migrateRegistryAccessSASecrets_2_40_0)
|
||||
|
||||
m.addMigrations("2.43.0",
|
||||
m.migrateGitConfigToSources_2_43_0,
|
||||
m.migrateCustomTemplateGitConfigToSources_2_43_0,
|
||||
)
|
||||
|
||||
// WARNING: do not change migrations that have already been released!
|
||||
|
||||
// Add new migrations above...
|
||||
// One function per migration, each versions migration funcs in the same file.
|
||||
}
|
||||
|
||||
// Always is always run at the end of migrations
|
||||
func (m *Migrator) Always() error {
|
||||
// currently nothing to be done in CE... yet
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbTooOldError() error {
|
||||
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support")
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
"github.com/portainer/portainer/api/pendingactions/handlers"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type cleanNAPWithOverridePolicies struct {
|
||||
EndpointGroupID portainer.EndpointGroupID
|
||||
}
|
||||
|
||||
func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("test ConvertCleanNAPWithOverridePoliciesPayload", func(t *testing.T) {
|
||||
|
||||
_, store := MustNewTestStore(t, true, false)
|
||||
defer func() {
|
||||
err := store.Close()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
gid := portainer.EndpointGroupID(1)
|
||||
|
||||
testData := []struct {
|
||||
Name string
|
||||
PendingAction portainer.PendingAction
|
||||
Expected any
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
Name: "test actiondata with EndpointGroupID 1",
|
||||
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
|
||||
1,
|
||||
&gid,
|
||||
),
|
||||
Expected: portainer.EndpointGroupID(1),
|
||||
},
|
||||
{
|
||||
Name: "test actionData nil",
|
||||
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
|
||||
2,
|
||||
nil,
|
||||
),
|
||||
Expected: nil,
|
||||
},
|
||||
{
|
||||
Name: "test actionData empty and expected error",
|
||||
PendingAction: portainer.PendingAction{
|
||||
EndpointID: 2,
|
||||
Action: actions.CleanNAPWithOverridePolicies,
|
||||
ActionData: "",
|
||||
},
|
||||
Expected: nil,
|
||||
Err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, d := range testData {
|
||||
err := store.PendingActions().Create(&d.PendingAction)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
pendingActions, err := store.PendingActions().ReadAll()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, endpointPendingAction := range pendingActions {
|
||||
t.Run(d.Name, func(t *testing.T) {
|
||||
if endpointPendingAction.Action == actions.CleanNAPWithOverridePolicies {
|
||||
var payload cleanNAPWithOverridePolicies
|
||||
|
||||
err := endpointPendingAction.UnmarshallActionData(&payload)
|
||||
|
||||
if d.Err && err == nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if d.Expected == nil && payload.EndpointGroupID != 0 {
|
||||
t.Errorf("expected nil, got %d", payload.EndpointGroupID)
|
||||
}
|
||||
|
||||
if d.Expected != nil {
|
||||
expected := d.Expected.(portainer.EndpointGroupID)
|
||||
if d.Expected != nil && expected != payload.EndpointGroupID {
|
||||
t.Errorf("expected EndpointGroupID %d, got %d", expected, payload.EndpointGroupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
err = store.PendingActions().Delete(d.PendingAction.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package postinit
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerClient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
"github.com/portainer/portainer/pkg/endpoints"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PostInitMigrator struct {
|
||||
kubeFactory *cli.ClientFactory
|
||||
dockerFactory *dockerClient.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
assetsPath string
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
}
|
||||
|
||||
func NewPostInitMigrator(
|
||||
kubeFactory *cli.ClientFactory,
|
||||
dockerFactory *dockerClient.ClientFactory,
|
||||
dataStore dataservices.DataStore,
|
||||
assetsPath string,
|
||||
kubernetesDeployer portainer.KubernetesDeployer,
|
||||
) *PostInitMigrator {
|
||||
return &PostInitMigrator{
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
dataStore: dataStore,
|
||||
assetsPath: assetsPath,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
}
|
||||
}
|
||||
|
||||
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
|
||||
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
|
||||
var environments []portainer.Endpoint
|
||||
|
||||
if err := postInitMigrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
if environments, err = tx.Endpoint().ReadAll(func(endpoint portainer.Endpoint) bool {
|
||||
return endpoints.HasDirectConnectivity(&endpoint)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to retrieve environments: %w", err)
|
||||
}
|
||||
|
||||
var pendingActions []portainer.PendingAction
|
||||
if pendingActions, err = tx.PendingActions().ReadAll(func(action portainer.PendingAction) bool {
|
||||
return action.Action == actions.PostInitMigrateEnvironment
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to retrieve pending actions: %w", err)
|
||||
}
|
||||
|
||||
// Sort for the binary search in createPostInitMigrationPendingAction()
|
||||
slices.SortFunc(pendingActions, func(a, b portainer.PendingAction) int {
|
||||
return cmp.Compare(a.EndpointID, b.EndpointID)
|
||||
})
|
||||
|
||||
for _, environment := range environments {
|
||||
if !endpoints.IsEdgeEndpoint(&environment) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Edge environments will run after the server starts, in pending actions
|
||||
log.Info().
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("adding pending action 'PostInitMigrateEnvironment' for environment")
|
||||
|
||||
if err := postInitMigrator.createPostInitMigrationPendingAction(tx, environment.ID, pendingActions); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error creating pending action for environment")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("error running post-init migrations")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
for _, environment := range environments {
|
||||
if endpoints.IsEdgeEndpoint(&environment) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Non-edge environments will run before the server starts.
|
||||
if err := postInitMigrator.MigrateEnvironment(&environment); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error running post-init migrations for non-edge environment")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// try to create a post init migration pending action. If it already exists, do nothing
|
||||
// this function exists for readability, not reusability
|
||||
// pending actions must be passed in ascending order by endpoint ID
|
||||
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(tx dataservices.DataStoreTx, environmentID portainer.EndpointID, pendingActions []portainer.PendingAction) error {
|
||||
action := portainer.PendingAction{
|
||||
EndpointID: environmentID,
|
||||
Action: actions.PostInitMigrateEnvironment,
|
||||
}
|
||||
|
||||
if _, found := slices.BinarySearchFunc(pendingActions, environmentID, func(e portainer.PendingAction, id portainer.EndpointID) int {
|
||||
return cmp.Compare(e.EndpointID, id)
|
||||
}); found {
|
||||
log.Debug().
|
||||
Str("action", action.Action).
|
||||
Int("endpoint_id", int(action.EndpointID)).
|
||||
Msg("pending action already exists for environment, skipping...")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return tx.PendingActions().Create(&action)
|
||||
}
|
||||
|
||||
// MigrateEnvironment runs migrations on a single environment
|
||||
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
|
||||
log.Info().
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("executing post init migration for environment")
|
||||
|
||||
switch {
|
||||
case endpointutils.IsKubernetesEndpoint(environment):
|
||||
// get the kubeclient for the environment, and skip all kube migrations if there's an error
|
||||
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error creating kubeclient for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// If one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
|
||||
var latestErr error
|
||||
kubernetesMigrations := []func() error{
|
||||
func() error { return migrator.MigrateIngresses(*environment, kubeclient) },
|
||||
func() error { return migrator.MigrateRegistrySASecrets(*environment, kubeclient) },
|
||||
}
|
||||
|
||||
for _, migration := range kubernetesMigrations {
|
||||
if err := migration(); err != nil {
|
||||
latestErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return latestErr
|
||||
case endpointutils.IsDockerEndpoint(environment):
|
||||
// get the docker client for the environment, and skip all docker migrations if there's an error
|
||||
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error creating docker client for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
defer logs.CloseAndLogErr(dockerClient)
|
||||
|
||||
if err := migrator.MigrateGPUs(*environment, dockerClient); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error migrating GPUs for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) MigrateRegistrySASecrets(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
|
||||
if !environment.PostInitMigrations.MigrateRegistrySASecrets {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("migrating registry SA secrets for environment")
|
||||
|
||||
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
env, err := tx.Endpoint().Endpoint(environment.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !env.PostInitMigrations.MigrateRegistrySASecrets {
|
||||
return nil
|
||||
}
|
||||
|
||||
registries, err := tx.Registry().ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
access, ok := registry.RegistryAccesses[env.ID]
|
||||
if !ok || len(access.Namespaces) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
secretName := registryutils.RegistrySecretName(registry.ID)
|
||||
for _, namespace := range access.Namespaces {
|
||||
if err := kubeclient.AddImagePullSecretToServiceAccount(namespace, "default", secretName); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(env.ID)).
|
||||
Str("namespace", namespace).
|
||||
Str("secret", secretName).
|
||||
Msg("failed to add imagePullSecret to service account during registry SA secret migration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env.PostInitMigrations.MigrateRegistrySASecrets = false
|
||||
return tx.Endpoint().UpdateEndpoint(env.ID, env)
|
||||
})
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
|
||||
// Early exit if we do not need to migrate!
|
||||
if !environment.PostInitMigrations.MigrateIngresses {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("migrating ingresses for environment")
|
||||
|
||||
if err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error migrating ingresses for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
|
||||
// If there's an error getting the containers, we'll log it and move on
|
||||
func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient *client.Client) error {
|
||||
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
environment, err := tx.Endpoint().Endpoint(e.ID)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(e.ID)).
|
||||
Msg("error getting environment")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Early exit if we do not need to migrate!
|
||||
if !environment.PostInitMigrations.MigrateGPUs {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(e.ID)).
|
||||
Msg("migrating GPUs for environment")
|
||||
|
||||
// Get all containers
|
||||
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("failed to list containers for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
|
||||
containersLoop:
|
||||
for _, container := range containers {
|
||||
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
|
||||
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to inspect container")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
deviceRequests := containerDetails.HostConfig.DeviceRequests
|
||||
for _, deviceRequest := range deviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" {
|
||||
environment.EnableGPUManagement = true
|
||||
|
||||
break containersLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the MigrateGPUs flag to false so we don't run this again
|
||||
environment.PostInitMigrations.MigrateGPUs = false
|
||||
if err := tx.Endpoint().UpdateEndpoint(environment.ID, environment); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Int("endpoint_id", int(environment.ID)).
|
||||
Msg("error updating EnableGPUManagement flag for environment")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package postinit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrateGPUs(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/containers/json") {
|
||||
containerSummary := []container.Summary{{ID: "container1"}}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(containerSummary); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
container := container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
ID: "container1",
|
||||
HostConfig: &container.HostConfig{
|
||||
Resources: container.Resources{
|
||||
DeviceRequests: []container.DeviceRequest{
|
||||
{Driver: "nvidia"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(container); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
migrator := &PostInitMigrator{dataStore: store}
|
||||
|
||||
dockerCli, err := client.NewClientWithOpts(client.WithHost(srv.URL), client.WithHTTPClient(http.DefaultClient))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Nonexistent endpoint
|
||||
|
||||
err = migrator.MigrateGPUs(portainer.Endpoint{}, dockerCli)
|
||||
require.Error(t, err)
|
||||
|
||||
// Valid endpoint
|
||||
|
||||
endpoint := portainer.Endpoint{ID: 1, PostInitMigrations: portainer.EndpointPostInitMigrations{MigrateGPUs: true}}
|
||||
|
||||
err = store.Endpoint().Create(&endpoint)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = migrator.MigrateGPUs(endpoint, dockerCli)
|
||||
require.NoError(t, err)
|
||||
|
||||
migratedEndpoint, err := store.Endpoint().Endpoint(endpoint.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, endpoint.ID, migratedEndpoint.ID)
|
||||
require.False(t, migratedEndpoint.PostInitMigrations.MigrateGPUs)
|
||||
require.True(t, migratedEndpoint.EnableGPUManagement)
|
||||
}
|
||||
|
||||
func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
existingPendingActions []*portainer.PendingAction
|
||||
expectedPendingActions int
|
||||
expectedAction string
|
||||
}{
|
||||
{
|
||||
name: "when existing non-matching action exists, should add migration action",
|
||||
existingPendingActions: []*portainer.PendingAction{
|
||||
{
|
||||
EndpointID: 7,
|
||||
Action: "some-other-action",
|
||||
},
|
||||
},
|
||||
expectedPendingActions: 2,
|
||||
expectedAction: actions.PostInitMigrateEnvironment,
|
||||
},
|
||||
{
|
||||
name: "when matching action exists, should not add duplicate",
|
||||
existingPendingActions: []*portainer.PendingAction{
|
||||
{
|
||||
EndpointID: 7,
|
||||
Action: actions.PostInitMigrateEnvironment,
|
||||
},
|
||||
},
|
||||
expectedPendingActions: 1,
|
||||
expectedAction: actions.PostInitMigrateEnvironment,
|
||||
},
|
||||
{
|
||||
name: "when no actions exist, should add migration action",
|
||||
existingPendingActions: []*portainer.PendingAction{},
|
||||
expectedPendingActions: 1,
|
||||
expectedAction: actions.PostInitMigrateEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
// Create test endpoint
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 7,
|
||||
UserTrusted: true,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
Edge: portainer.EnvironmentEdgeSettings{
|
||||
AsyncMode: false,
|
||||
},
|
||||
EdgeID: "edgeID",
|
||||
}
|
||||
err := store.Endpoint().Create(endpoint)
|
||||
require.NoError(t, err, "error creating endpoint")
|
||||
|
||||
// Create any existing pending actions
|
||||
for _, action := range tt.existingPendingActions {
|
||||
err = store.PendingActions().Create(action)
|
||||
require.NoError(t, err, "error creating pending action")
|
||||
}
|
||||
|
||||
migrator := NewPostInitMigrator(
|
||||
nil, // kubeFactory not needed for this test
|
||||
nil, // dockerFactory not needed for this test
|
||||
store,
|
||||
"", // assetsPath not needed for this test
|
||||
nil, // kubernetesDeployer not needed for this test
|
||||
)
|
||||
|
||||
err = migrator.PostInitMigrate()
|
||||
require.NoError(t, err, "PostInitMigrate should not return error")
|
||||
|
||||
// Verify the results
|
||||
pendingActions, err := store.PendingActions().ReadAll()
|
||||
require.NoError(t, err, "error reading pending actions")
|
||||
is.Len(pendingActions, tt.expectedPendingActions, "unexpected number of pending actions")
|
||||
|
||||
// If we expect any actions, verify at least one has the expected action type
|
||||
if tt.expectedPendingActions > 0 {
|
||||
hasExpectedAction := false
|
||||
for _, action := range pendingActions {
|
||||
if action.Action == tt.expectedAction {
|
||||
hasExpectedAction = true
|
||||
is.Equal(endpoint.ID, action.EndpointID, "action should reference correct endpoint")
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
is.True(hasExpectedAction, "should have found action of expected type")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/dataservices/allowlist"
|
||||
"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/edgestackstatus"
|
||||
"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/pendingactions"
|
||||
"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/snapshot"
|
||||
"github.com/portainer/portainer/api/dataservices/source"
|
||||
"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/portainer/portainer/api/dataservices/workflow"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
var _ dataservices.DataStore = &Store{}
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
flags *portainer.CLIFlags
|
||||
connection portainer.Connection
|
||||
|
||||
fileService portainer.FileService
|
||||
AllowListService *allowlist.Service
|
||||
CustomTemplateService *customtemplate.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeStackStatusService *edgestackstatus.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
|
||||
SnapshotService *snapshot.Service
|
||||
SourceService *source.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
|
||||
WorkflowService *workflow.Service
|
||||
PendingActionsService *pendingactions.Service
|
||||
}
|
||||
|
||||
func (store *Store) initServices() error {
|
||||
allowListService, err := allowlist.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.AllowListService = allowListService
|
||||
|
||||
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
|
||||
|
||||
endpointRelationService, err := endpointrelation.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EndpointRelationService = endpointRelationService
|
||||
|
||||
edgeStackService, err := edgestack.NewService(store.connection, func(tx portainer.Transaction, ID portainer.EdgeStackID) {
|
||||
endpointRelationService.Tx(tx).InvalidateEdgeCacheForEdgeStack(ID)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EdgeStackService = edgeStackService
|
||||
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFuncTx)
|
||||
|
||||
edgeStackStatusService, err := edgestackstatus.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EdgeStackStatusService = edgeStackStatusService
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
snapshotService, err := snapshot.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.SnapshotService = snapshotService
|
||||
|
||||
sourceService, err := source.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.SourceService = sourceService
|
||||
|
||||
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
|
||||
|
||||
workflowService, err := workflow.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.WorkflowService = workflowService
|
||||
|
||||
scheduleService, err := schedule.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.ScheduleService = scheduleService
|
||||
|
||||
pendingActionsService, err := pendingactions.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.PendingActionsService = pendingActionsService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PendingActions gives access to the PendingActions data management layer
|
||||
func (store *Store) PendingActions() dataservices.PendingActionsService {
|
||||
return store.PendingActionsService
|
||||
}
|
||||
|
||||
// AllowList gives access to the AllowList data management layer
|
||||
func (store *Store) AllowList() dataservices.AllowListService {
|
||||
return store.AllowListService
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (store *Store) EdgeStackStatus() dataservices.EdgeStackStatusService {
|
||||
return store.EdgeStackStatusService
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (store *Store) Snapshot() dataservices.SnapshotService {
|
||||
return store.SnapshotService
|
||||
}
|
||||
|
||||
// Source gives access to the Source data management layer
|
||||
func (store *Store) Source() dataservices.SourceService {
|
||||
return store.SourceService
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Workflow gives access to the Workflow data management layer
|
||||
func (store *Store) Workflow() dataservices.WorkflowService {
|
||||
return store.WorkflowService
|
||||
}
|
||||
|
||||
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,omitzero"`
|
||||
Snapshot []portainer.Snapshot `json:"snapshots,omitempty"`
|
||||
SSLSettings portainer.SSLSettings `json:"ssl,omitzero"`
|
||||
Source []portainer.Source `json:"sources,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,omitzero"`
|
||||
User []portainer.User `json:"users,omitempty"`
|
||||
Version models.Version `json:"version,omitzero"`
|
||||
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
|
||||
Workflow []portainer.Workflow `json:"workflows,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (store *Store) Export(filename string) (err error) {
|
||||
backup := storeExport{}
|
||||
|
||||
if c, err := store.CustomTemplate().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Custom Templates")
|
||||
}
|
||||
} else {
|
||||
backup.CustomTemplate = c
|
||||
}
|
||||
|
||||
if e, err := store.EdgeGroup().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Edge Groups")
|
||||
}
|
||||
} else {
|
||||
backup.EdgeGroup = e
|
||||
}
|
||||
|
||||
if e, err := store.EdgeJob().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Edge Jobs")
|
||||
}
|
||||
} else {
|
||||
backup.EdgeJob = e
|
||||
}
|
||||
|
||||
if e, err := store.EdgeStack().EdgeStacks(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Edge Stacks")
|
||||
}
|
||||
} else {
|
||||
backup.EdgeStack = e
|
||||
}
|
||||
|
||||
if e, err := store.Endpoint().Endpoints(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Endpoints")
|
||||
}
|
||||
} else {
|
||||
backup.Endpoint = e
|
||||
}
|
||||
|
||||
if e, err := store.EndpointGroup().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Endpoint Groups")
|
||||
}
|
||||
} else {
|
||||
backup.EndpointGroup = e
|
||||
}
|
||||
|
||||
if r, err := store.EndpointRelation().EndpointRelations(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Endpoint Relations")
|
||||
}
|
||||
} else {
|
||||
backup.EndpointRelation = r
|
||||
}
|
||||
|
||||
if r, err := store.ExtensionService.Extensions(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Extensions")
|
||||
}
|
||||
} else {
|
||||
backup.Extensions = r
|
||||
}
|
||||
|
||||
if r, err := store.HelmUserRepository().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Helm User Repositories")
|
||||
}
|
||||
} else {
|
||||
backup.HelmUserRepository = r
|
||||
}
|
||||
|
||||
if r, err := store.Registry().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Registries")
|
||||
}
|
||||
} else {
|
||||
backup.Registry = r
|
||||
}
|
||||
|
||||
if c, err := store.ResourceControl().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Resource Controls")
|
||||
}
|
||||
} else {
|
||||
backup.ResourceControl = c
|
||||
}
|
||||
|
||||
if role, err := store.Role().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Roles")
|
||||
}
|
||||
} else {
|
||||
backup.Role = role
|
||||
}
|
||||
|
||||
if r, err := store.ScheduleService.Schedules(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Schedules")
|
||||
}
|
||||
} else {
|
||||
backup.Schedules = r
|
||||
}
|
||||
|
||||
if settings, err := store.Settings().Settings(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Settings")
|
||||
}
|
||||
} else {
|
||||
backup.Settings = *settings
|
||||
}
|
||||
|
||||
if snapshot, err := store.Snapshot().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Snapshots")
|
||||
}
|
||||
} else {
|
||||
backup.Snapshot = snapshot
|
||||
}
|
||||
|
||||
if settings, err := store.SSLSettings().Settings(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting SSL Settings")
|
||||
}
|
||||
} else {
|
||||
backup.SSLSettings = *settings
|
||||
}
|
||||
|
||||
if s, err := store.Source().ReadAll(source.InsecureNewAdminContext()); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Sources")
|
||||
}
|
||||
} else {
|
||||
backup.Source = s
|
||||
}
|
||||
|
||||
if t, err := store.Stack().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Stacks")
|
||||
}
|
||||
} else {
|
||||
backup.Stack = t
|
||||
}
|
||||
|
||||
if t, err := store.Tag().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Tags")
|
||||
}
|
||||
} else {
|
||||
backup.Tag = t
|
||||
}
|
||||
|
||||
if t, err := store.TeamMembership().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Team Memberships")
|
||||
}
|
||||
} else {
|
||||
backup.TeamMembership = t
|
||||
}
|
||||
|
||||
if t, err := store.Team().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Teams")
|
||||
}
|
||||
} else {
|
||||
backup.Team = t
|
||||
}
|
||||
|
||||
if info, err := store.TunnelServer().Info(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Tunnel Server")
|
||||
}
|
||||
} else {
|
||||
backup.TunnelServer = *info
|
||||
}
|
||||
|
||||
if users, err := store.User().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Users")
|
||||
}
|
||||
} else {
|
||||
backup.User = users
|
||||
}
|
||||
|
||||
if webhooks, err := store.Webhook().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Webhooks")
|
||||
}
|
||||
} else {
|
||||
backup.Webhook = webhooks
|
||||
}
|
||||
|
||||
if w, err := store.Workflow().ReadAll(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Workflows")
|
||||
}
|
||||
} else {
|
||||
backup.Workflow = w
|
||||
}
|
||||
|
||||
if version, err := store.Version().Version(); err != nil {
|
||||
if !store.IsErrObjectNotFound(err) {
|
||||
log.Error().Err(err).Msg("exporting Version")
|
||||
}
|
||||
} else {
|
||||
backup.Version = *version
|
||||
}
|
||||
|
||||
backup.Metadata, err = store.connection.BackupMetadata()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("exporting Metadata")
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(backup, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, b, 0o600)
|
||||
}
|
||||
|
||||
func (store *Store) Import(filename string) (err error) {
|
||||
backup := storeExport{}
|
||||
|
||||
s, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(s, &backup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.Version().UpdateVersion(&backup.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range backup.CustomTemplate {
|
||||
if err := store.CustomTemplate().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the custom template in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.EdgeGroup {
|
||||
if err := store.EdgeGroup().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the edge group in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.EdgeJob {
|
||||
if err := store.EdgeJob().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the edge job in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.EdgeStack {
|
||||
if err := store.EdgeStack().UpdateEdgeStack(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the edge stack in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Endpoint {
|
||||
if err := store.Endpoint().UpdateEndpoint(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the endpoint in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.EndpointGroup {
|
||||
if err := store.EndpointGroup().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the endpoint group in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.EndpointRelation {
|
||||
if err := store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the endpoint relation in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.HelmUserRepository {
|
||||
if err := store.HelmUserRepository().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the helm user repository in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Registry {
|
||||
if err := store.Registry().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the registry in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.ResourceControl {
|
||||
if err := store.ResourceControl().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the resource control in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Role {
|
||||
if err := store.Role().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the role in the database")
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.Settings().UpdateSettings(&backup.Settings); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the settings in the database")
|
||||
}
|
||||
|
||||
if err := store.SSLSettings().UpdateSettings(&backup.SSLSettings); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the SSL settings in the database")
|
||||
}
|
||||
|
||||
for _, v := range backup.Snapshot {
|
||||
if err := store.Snapshot().Update(v.EndpointID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the snapshot in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Source {
|
||||
if err := store.Source().Update(source.InsecureNewAdminContext(), v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the source in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Workflow {
|
||||
if err := store.Workflow().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the workflow in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Stack {
|
||||
if err := store.Stack().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the stack in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Tag {
|
||||
if err := store.Tag().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the tag in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.TeamMembership {
|
||||
if err := store.TeamMembership().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the team membership in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Team {
|
||||
if err := store.Team().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the team in the database")
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.TunnelServer().UpdateInfo(&backup.TunnelServer); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the tunnel server info in the database")
|
||||
}
|
||||
|
||||
for _, user := range backup.User {
|
||||
if err := store.User().Update(user.ID, &user); err != nil {
|
||||
log.Warn().Str("user", fmt.Sprintf("%+v", user)).Err(err).Msg("failed to update the user in the database")
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range backup.Webhook {
|
||||
if err := store.Webhook().Update(v.ID, &v); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to update the webhook in the database")
|
||||
}
|
||||
}
|
||||
|
||||
return store.connection.RestoreMetadata(backup.Metadata)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type StoreTx struct {
|
||||
store *Store
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
|
||||
return tx.store.IsErrObjectNotFound(err)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) AllowList() dataservices.AllowListService {
|
||||
return tx.store.AllowListService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService {
|
||||
return tx.store.CustomTemplateService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService {
|
||||
return tx.store.PendingActionsService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
|
||||
return tx.store.EdgeGroupService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeJob() dataservices.EdgeJobService {
|
||||
return tx.store.EdgeJobService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeStack() dataservices.EdgeStackService {
|
||||
return tx.store.EdgeStackService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeStackStatus() dataservices.EdgeStackStatusService {
|
||||
return tx.store.EdgeStackStatusService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Endpoint() dataservices.EndpointService {
|
||||
return tx.store.EndpointService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EndpointGroup() dataservices.EndpointGroupService {
|
||||
return tx.store.EndpointGroupService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
|
||||
return tx.store.EndpointRelationService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
||||
|
||||
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
||||
return tx.store.RegistryService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) ResourceControl() dataservices.ResourceControlService {
|
||||
return tx.store.ResourceControlService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Role() dataservices.RoleService {
|
||||
return tx.store.RoleService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) APIKeyRepository() dataservices.APIKeyRepository { return nil }
|
||||
|
||||
func (tx *StoreTx) Settings() dataservices.SettingsService {
|
||||
return tx.store.SettingsService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
|
||||
return tx.store.SnapshotService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Source() dataservices.SourceService {
|
||||
return tx.store.SourceService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService {
|
||||
return tx.store.SSLSettingsService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Stack() dataservices.StackService {
|
||||
return tx.store.StackService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Tag() dataservices.TagService {
|
||||
return tx.store.TagService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService {
|
||||
return tx.store.TeamMembershipService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Team() dataservices.TeamService {
|
||||
return tx.store.TeamService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }
|
||||
|
||||
func (tx *StoreTx) User() dataservices.UserService {
|
||||
return tx.store.UserService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Version() dataservices.VersionService { return nil }
|
||||
func (tx *StoreTx) Webhook() dataservices.WebhookService { return nil }
|
||||
|
||||
func (tx *StoreTx) Workflow() dataservices.WorkflowService {
|
||||
return tx.store.WorkflowService.Tx(tx.tx)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,928 @@
|
||||
{
|
||||
"allowlist": null,
|
||||
"api_key": null,
|
||||
"customtemplates": null,
|
||||
"dockerhub": [
|
||||
{
|
||||
"Authentication": false,
|
||||
"Username": ""
|
||||
}
|
||||
],
|
||||
"edge_stack": null,
|
||||
"edge_stack_status": null,
|
||||
"edgegroups": null,
|
||||
"edgejobs": null,
|
||||
"endpoint_groups": [
|
||||
{
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"Description": "Unassigned endpoints",
|
||||
"Id": 1,
|
||||
"Labels": [],
|
||||
"Name": "Unassigned",
|
||||
"TagIds": [],
|
||||
"Tags": null,
|
||||
"TeamAccessPolicies": {},
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
],
|
||||
"endpoint_relations": [
|
||||
{
|
||||
"EdgeStacks": {},
|
||||
"EndpointID": 1
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"Agent": {},
|
||||
"AzureCredentials": {
|
||||
"ApplicationID": "",
|
||||
"AuthenticationKey": "",
|
||||
"TenantID": ""
|
||||
},
|
||||
"ComposeSyntaxMaxVersion": "",
|
||||
"ContainerEngine": "",
|
||||
"Edge": {
|
||||
"AsyncMode": false,
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeCheckinInterval": 0,
|
||||
"EdgeKey": "",
|
||||
"GroupId": 1,
|
||||
"Heartbeat": false,
|
||||
"Id": 1,
|
||||
"Kubernetes": {
|
||||
"Configuration": {
|
||||
"AllowNoneIngressClass": false,
|
||||
"EnableResourceOverCommit": false,
|
||||
"IngressAvailabilityPerNamespace": true,
|
||||
"ResourceOverCommitPercentage": 0,
|
||||
"RestrictDefaultNamespace": false,
|
||||
"UseLoadBalancer": false,
|
||||
"UseServerMetrics": false
|
||||
},
|
||||
"Flags": {
|
||||
"IsServerIngressClassDetected": false,
|
||||
"IsServerMetricsDetected": false,
|
||||
"IsServerStorageDetected": false
|
||||
}
|
||||
},
|
||||
"LastCheckInDate": 0,
|
||||
"Name": "local",
|
||||
"PostInitMigrations": {
|
||||
"MigrateGPUs": true,
|
||||
"MigrateIngresses": true,
|
||||
"MigrateRegistrySASecrets": false
|
||||
},
|
||||
"PublicURL": "",
|
||||
"SecuritySettings": {
|
||||
"allowBindMountsForRegularUsers": true,
|
||||
"allowContainerCapabilitiesForRegularUsers": true,
|
||||
"allowDeviceMappingForRegularUsers": true,
|
||||
"allowHostNamespaceForRegularUsers": true,
|
||||
"allowPrivilegedModeForRegularUsers": true,
|
||||
"allowSecurityOptForRegularUsers": false,
|
||||
"allowStackManagementForRegularUsers": true,
|
||||
"allowSysctlSettingForRegularUsers": false,
|
||||
"allowVolumeBrowserForRegularUsers": false,
|
||||
"enableHostManagementFeatures": false
|
||||
},
|
||||
"Status": 1,
|
||||
"TLSConfig": {
|
||||
"TLS": false,
|
||||
"TLSSkipVerify": false
|
||||
},
|
||||
"Type": 1,
|
||||
"URL": "unix:///var/run/docker.sock"
|
||||
}
|
||||
],
|
||||
"extension": null,
|
||||
"helm_user_repository": null,
|
||||
"pending_actions": null,
|
||||
"registries": [
|
||||
{
|
||||
"Authentication": true,
|
||||
"AuthorizedTeams": null,
|
||||
"AuthorizedUsers": null,
|
||||
"BaseURL": "",
|
||||
"Ecr": {
|
||||
"Region": ""
|
||||
},
|
||||
"Github": {
|
||||
"OrganisationName": "",
|
||||
"UseOrganisation": false
|
||||
},
|
||||
"Gitlab": {
|
||||
"InstanceURL": "",
|
||||
"ProjectId": 0,
|
||||
"ProjectPath": ""
|
||||
},
|
||||
"Id": 1,
|
||||
"ManagementConfiguration": null,
|
||||
"Name": "canister.io",
|
||||
"Password": "MjWbx8A6YK7cw7",
|
||||
"Quay": {
|
||||
"OrganisationName": ""
|
||||
},
|
||||
"RegistryAccesses": {
|
||||
"1": {
|
||||
"Namespaces": [],
|
||||
"TeamAccessPolicies": {},
|
||||
"UserAccessPolicies": {}
|
||||
}
|
||||
},
|
||||
"TeamAccessPolicies": {},
|
||||
"Type": 3,
|
||||
"URL": "cloud.canister.io:5000",
|
||||
"UserAccessPolicies": {},
|
||||
"Username": "prabhatkhera"
|
||||
}
|
||||
],
|
||||
"resource_control": [
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 2,
|
||||
"Public": true,
|
||||
"ResourceId": "762gbwaj8r4gcsdy8ld1u4why",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 5,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 3,
|
||||
"Public": true,
|
||||
"ResourceId": "1_alpine",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 4,
|
||||
"Public": true,
|
||||
"ResourceId": "1_redis",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
},
|
||||
{
|
||||
"AdministratorsOnly": false,
|
||||
"Id": 5,
|
||||
"Public": false,
|
||||
"ResourceId": "1_nginx",
|
||||
"SubResourceIds": [],
|
||||
"System": false,
|
||||
"TeamAccesses": [
|
||||
{
|
||||
"AccessLevel": 1,
|
||||
"TeamId": 1
|
||||
}
|
||||
],
|
||||
"Type": 6,
|
||||
"UserAccesses": []
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentBrowseDelete": true,
|
||||
"DockerAgentBrowseGet": true,
|
||||
"DockerAgentBrowseList": true,
|
||||
"DockerAgentBrowsePut": true,
|
||||
"DockerAgentBrowseRename": true,
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerAgentUndefined": true,
|
||||
"DockerBuildCancel": true,
|
||||
"DockerBuildPrune": true,
|
||||
"DockerConfigCreate": true,
|
||||
"DockerConfigDelete": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerConfigUpdate": true,
|
||||
"DockerContainerArchive": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerAttach": true,
|
||||
"DockerContainerAttachWebsocket": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerCreate": true,
|
||||
"DockerContainerDelete": true,
|
||||
"DockerContainerExec": true,
|
||||
"DockerContainerExport": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerKill": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerPause": true,
|
||||
"DockerContainerPrune": true,
|
||||
"DockerContainerPutContainerArchive": true,
|
||||
"DockerContainerRename": true,
|
||||
"DockerContainerResize": true,
|
||||
"DockerContainerRestart": true,
|
||||
"DockerContainerStart": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerStop": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerContainerUnpause": true,
|
||||
"DockerContainerUpdate": true,
|
||||
"DockerContainerWait": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerExecInspect": true,
|
||||
"DockerExecResize": true,
|
||||
"DockerExecStart": true,
|
||||
"DockerImageBuild": true,
|
||||
"DockerImageCommit": true,
|
||||
"DockerImageCreate": true,
|
||||
"DockerImageDelete": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageLoad": true,
|
||||
"DockerImagePrune": true,
|
||||
"DockerImagePush": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerImageTag": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkConnect": true,
|
||||
"DockerNetworkCreate": true,
|
||||
"DockerNetworkDelete": true,
|
||||
"DockerNetworkDisconnect": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNetworkPrune": true,
|
||||
"DockerNodeDelete": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerNodeUpdate": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginCreate": true,
|
||||
"DockerPluginDelete": true,
|
||||
"DockerPluginDisable": true,
|
||||
"DockerPluginEnable": true,
|
||||
"DockerPluginInspect": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerPluginPrivileges": true,
|
||||
"DockerPluginPull": true,
|
||||
"DockerPluginPush": true,
|
||||
"DockerPluginSet": true,
|
||||
"DockerPluginUpgrade": true,
|
||||
"DockerSecretCreate": true,
|
||||
"DockerSecretDelete": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerSecretUpdate": true,
|
||||
"DockerServiceCreate": true,
|
||||
"DockerServiceDelete": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerServiceUpdate": true,
|
||||
"DockerSessionStart": true,
|
||||
"DockerSwarmInit": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSwarmJoin": true,
|
||||
"DockerSwarmLeave": true,
|
||||
"DockerSwarmUnlock": true,
|
||||
"DockerSwarmUnlockKey": true,
|
||||
"DockerSwarmUpdate": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerUndefined": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeCreate": true,
|
||||
"DockerVolumeDelete": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"DockerVolumePrune": true,
|
||||
"EndpointResourcesAccess": true,
|
||||
"IntegrationStoridgeAdmin": true,
|
||||
"PortainerResourceControlCreate": true,
|
||||
"PortainerResourceControlUpdate": true,
|
||||
"PortainerStackCreate": true,
|
||||
"PortainerStackDelete": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerStackMigrate": true,
|
||||
"PortainerStackUpdate": true,
|
||||
"PortainerWebhookCreate": true,
|
||||
"PortainerWebhookDelete": true,
|
||||
"PortainerWebhookList": true,
|
||||
"PortainerWebsocketExec": true
|
||||
},
|
||||
"Description": "Full control of all resources in an endpoint",
|
||||
"Id": 1,
|
||||
"Name": "Endpoint administrator",
|
||||
"Priority": 1
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"EndpointResourcesAccess": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerWebhookList": true
|
||||
},
|
||||
"Description": "Read-only access of all resources in an endpoint",
|
||||
"Id": 2,
|
||||
"Name": "Helpdesk",
|
||||
"Priority": 2
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerAgentUndefined": true,
|
||||
"DockerBuildCancel": true,
|
||||
"DockerBuildPrune": true,
|
||||
"DockerConfigCreate": true,
|
||||
"DockerConfigDelete": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerConfigUpdate": true,
|
||||
"DockerContainerArchive": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerAttach": true,
|
||||
"DockerContainerAttachWebsocket": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerCreate": true,
|
||||
"DockerContainerDelete": true,
|
||||
"DockerContainerExec": true,
|
||||
"DockerContainerExport": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerKill": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerPause": true,
|
||||
"DockerContainerPutContainerArchive": true,
|
||||
"DockerContainerRename": true,
|
||||
"DockerContainerResize": true,
|
||||
"DockerContainerRestart": true,
|
||||
"DockerContainerStart": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerStop": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerContainerUnpause": true,
|
||||
"DockerContainerUpdate": true,
|
||||
"DockerContainerWait": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerExecInspect": true,
|
||||
"DockerExecResize": true,
|
||||
"DockerExecStart": true,
|
||||
"DockerImageBuild": true,
|
||||
"DockerImageCommit": true,
|
||||
"DockerImageCreate": true,
|
||||
"DockerImageDelete": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageLoad": true,
|
||||
"DockerImagePush": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerImageTag": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkConnect": true,
|
||||
"DockerNetworkCreate": true,
|
||||
"DockerNetworkDelete": true,
|
||||
"DockerNetworkDisconnect": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeDelete": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerNodeUpdate": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginCreate": true,
|
||||
"DockerPluginDelete": true,
|
||||
"DockerPluginDisable": true,
|
||||
"DockerPluginEnable": true,
|
||||
"DockerPluginInspect": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerPluginPrivileges": true,
|
||||
"DockerPluginPull": true,
|
||||
"DockerPluginPush": true,
|
||||
"DockerPluginSet": true,
|
||||
"DockerPluginUpgrade": true,
|
||||
"DockerSecretCreate": true,
|
||||
"DockerSecretDelete": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerSecretUpdate": true,
|
||||
"DockerServiceCreate": true,
|
||||
"DockerServiceDelete": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerServiceUpdate": true,
|
||||
"DockerSessionStart": true,
|
||||
"DockerSwarmInit": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSwarmJoin": true,
|
||||
"DockerSwarmLeave": true,
|
||||
"DockerSwarmUnlock": true,
|
||||
"DockerSwarmUnlockKey": true,
|
||||
"DockerSwarmUpdate": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerUndefined": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeCreate": true,
|
||||
"DockerVolumeDelete": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"PortainerResourceControlUpdate": true,
|
||||
"PortainerStackCreate": true,
|
||||
"PortainerStackDelete": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerStackMigrate": true,
|
||||
"PortainerStackUpdate": true,
|
||||
"PortainerWebhookCreate": true,
|
||||
"PortainerWebhookList": true,
|
||||
"PortainerWebsocketExec": true
|
||||
},
|
||||
"Description": "Full control of assigned resources in an endpoint",
|
||||
"Id": 3,
|
||||
"Name": "Standard user",
|
||||
"Priority": 3
|
||||
},
|
||||
{
|
||||
"Authorizations": {
|
||||
"DockerAgentHostInfo": true,
|
||||
"DockerAgentList": true,
|
||||
"DockerAgentPing": true,
|
||||
"DockerConfigInspect": true,
|
||||
"DockerConfigList": true,
|
||||
"DockerContainerArchiveInfo": true,
|
||||
"DockerContainerChanges": true,
|
||||
"DockerContainerInspect": true,
|
||||
"DockerContainerList": true,
|
||||
"DockerContainerLogs": true,
|
||||
"DockerContainerStats": true,
|
||||
"DockerContainerTop": true,
|
||||
"DockerDistributionInspect": true,
|
||||
"DockerEvents": true,
|
||||
"DockerImageGet": true,
|
||||
"DockerImageGetAll": true,
|
||||
"DockerImageHistory": true,
|
||||
"DockerImageInspect": true,
|
||||
"DockerImageList": true,
|
||||
"DockerImageSearch": true,
|
||||
"DockerInfo": true,
|
||||
"DockerNetworkInspect": true,
|
||||
"DockerNetworkList": true,
|
||||
"DockerNodeInspect": true,
|
||||
"DockerNodeList": true,
|
||||
"DockerPing": true,
|
||||
"DockerPluginList": true,
|
||||
"DockerSecretInspect": true,
|
||||
"DockerSecretList": true,
|
||||
"DockerServiceInspect": true,
|
||||
"DockerServiceList": true,
|
||||
"DockerServiceLogs": true,
|
||||
"DockerSwarmInspect": true,
|
||||
"DockerSystem": true,
|
||||
"DockerTaskInspect": true,
|
||||
"DockerTaskList": true,
|
||||
"DockerTaskLogs": true,
|
||||
"DockerVersion": true,
|
||||
"DockerVolumeInspect": true,
|
||||
"DockerVolumeList": true,
|
||||
"PortainerStackFile": true,
|
||||
"PortainerStackInspect": true,
|
||||
"PortainerStackList": true,
|
||||
"PortainerWebhookList": true
|
||||
},
|
||||
"Description": "Read-only access of assigned resources in an endpoint",
|
||||
"Id": 4,
|
||||
"Name": "Read-only user",
|
||||
"Priority": 4
|
||||
}
|
||||
],
|
||||
"schedules": [
|
||||
{
|
||||
"Created": 1648608136,
|
||||
"CronExpression": "@every 5m",
|
||||
"EdgeSchedule": null,
|
||||
"EndpointSyncJob": null,
|
||||
"Id": 1,
|
||||
"JobType": 2,
|
||||
"Name": "system_snapshot",
|
||||
"Recurring": true,
|
||||
"ScriptExecutionJob": null,
|
||||
"SnapshotJob": {}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"AgentSecret": "",
|
||||
"AllowBindMountsForRegularUsers": true,
|
||||
"AllowContainerCapabilitiesForRegularUsers": true,
|
||||
"AllowDeviceMappingForRegularUsers": true,
|
||||
"AllowHostNamespaceForRegularUsers": true,
|
||||
"AllowPrivilegedModeForRegularUsers": true,
|
||||
"AllowStackManagementForRegularUsers": true,
|
||||
"AuthenticationMethod": 1,
|
||||
"BlackListedLabels": [],
|
||||
"Edge": {
|
||||
"CommandInterval": 0,
|
||||
"PingInterval": 0,
|
||||
"SnapshotInterval": 0
|
||||
},
|
||||
"EdgeAgentCheckinInterval": 5,
|
||||
"EdgePortainerUrl": "",
|
||||
"EnableEdgeComputeFeatures": false,
|
||||
"EnforceEdgeID": false,
|
||||
"FeatureFlagSettings": null,
|
||||
"ForceSecureCookies": false,
|
||||
"GlobalDeploymentOptions": {
|
||||
"hideStacksFunctionality": false
|
||||
},
|
||||
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
|
||||
"InternalAuthSettings": {
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.43.0",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
"GroupSearchSettings": [
|
||||
{
|
||||
"GroupAttribute": "",
|
||||
"GroupBaseDN": "",
|
||||
"GroupFilter": ""
|
||||
}
|
||||
],
|
||||
"ReaderDN": "",
|
||||
"SearchSettings": [
|
||||
{
|
||||
"BaseDN": "",
|
||||
"Filter": "",
|
||||
"UserNameAttribute": ""
|
||||
}
|
||||
],
|
||||
"StartTLS": false,
|
||||
"TLSConfig": {
|
||||
"TLS": false,
|
||||
"TLSSkipVerify": false
|
||||
},
|
||||
"URL": ""
|
||||
},
|
||||
"LogoURL": "",
|
||||
"OAuthSettings": {
|
||||
"AccessTokenURI": "",
|
||||
"AuthStyle": 0,
|
||||
"AuthorizationURI": "",
|
||||
"ClientID": "",
|
||||
"DefaultTeamID": 0,
|
||||
"KubeSecretKey": null,
|
||||
"LogoutURI": "",
|
||||
"OAuthAutoCreateUsers": false,
|
||||
"RedirectURI": "",
|
||||
"ResourceURI": "",
|
||||
"SSO": false,
|
||||
"Scopes": "",
|
||||
"UserIdentifier": ""
|
||||
},
|
||||
"SnapshotInterval": "5m",
|
||||
"TemplatesURL": "",
|
||||
"TrustOnFirstConnect": false,
|
||||
"UserSessionTimeout": "8h"
|
||||
},
|
||||
"snapshots": [
|
||||
{
|
||||
"Docker": {
|
||||
"ContainerCount": 0,
|
||||
"DiagnosticsData": {},
|
||||
"DockerSnapshotRaw": {
|
||||
"Info": {
|
||||
"Architecture": "",
|
||||
"CDISpecDirs": null,
|
||||
"CPUSet": false,
|
||||
"CPUShares": false,
|
||||
"CgroupDriver": "",
|
||||
"ContainerdCommit": {
|
||||
"ID": ""
|
||||
},
|
||||
"Containers": 0,
|
||||
"ContainersPaused": 0,
|
||||
"ContainersRunning": 0,
|
||||
"ContainersStopped": 0,
|
||||
"CpuCfsPeriod": false,
|
||||
"CpuCfsQuota": false,
|
||||
"Debug": false,
|
||||
"DefaultRuntime": "",
|
||||
"DockerRootDir": "",
|
||||
"Driver": "",
|
||||
"DriverStatus": null,
|
||||
"ExperimentalBuild": false,
|
||||
"GenericResources": null,
|
||||
"HttpProxy": "",
|
||||
"HttpsProxy": "",
|
||||
"ID": "",
|
||||
"IPv4Forwarding": false,
|
||||
"Images": 0,
|
||||
"IndexServerAddress": "",
|
||||
"InitBinary": "",
|
||||
"InitCommit": {
|
||||
"ID": ""
|
||||
},
|
||||
"Isolation": "",
|
||||
"KernelVersion": "",
|
||||
"Labels": null,
|
||||
"LiveRestoreEnabled": false,
|
||||
"LoggingDriver": "",
|
||||
"MemTotal": 0,
|
||||
"MemoryLimit": false,
|
||||
"NCPU": 0,
|
||||
"NEventsListener": 0,
|
||||
"NFd": 0,
|
||||
"NGoroutines": 0,
|
||||
"Name": "",
|
||||
"NoProxy": "",
|
||||
"OSType": "",
|
||||
"OSVersion": "",
|
||||
"OomKillDisable": false,
|
||||
"OperatingSystem": "",
|
||||
"PidsLimit": false,
|
||||
"Plugins": {
|
||||
"Authorization": null,
|
||||
"Log": null,
|
||||
"Network": null,
|
||||
"Volume": null
|
||||
},
|
||||
"RegistryConfig": null,
|
||||
"RuncCommit": {
|
||||
"ID": ""
|
||||
},
|
||||
"Runtimes": null,
|
||||
"SecurityOptions": null,
|
||||
"ServerVersion": "",
|
||||
"SwapLimit": false,
|
||||
"Swarm": {
|
||||
"ControlAvailable": false,
|
||||
"Error": "",
|
||||
"LocalNodeState": "",
|
||||
"NodeAddr": "",
|
||||
"NodeID": "",
|
||||
"RemoteManagers": null
|
||||
},
|
||||
"SystemTime": "",
|
||||
"Warnings": null
|
||||
},
|
||||
"Version": {
|
||||
"ApiVersion": "",
|
||||
"Arch": "",
|
||||
"GitCommit": "",
|
||||
"GoVersion": "",
|
||||
"Os": "",
|
||||
"Platform": {
|
||||
"Name": ""
|
||||
},
|
||||
"Version": ""
|
||||
},
|
||||
"Volumes": {
|
||||
"Volumes": null,
|
||||
"Warnings": null
|
||||
}
|
||||
},
|
||||
"DockerVersion": "20.10.13",
|
||||
"GpuUseAll": false,
|
||||
"HealthyContainerCount": 0,
|
||||
"ImageCount": 9,
|
||||
"IsPodman": false,
|
||||
"NodeCount": 0,
|
||||
"RunningContainerCount": 5,
|
||||
"ServiceCount": 0,
|
||||
"StackCount": 2,
|
||||
"StoppedContainerCount": 0,
|
||||
"Swarm": false,
|
||||
"Time": 1648610112,
|
||||
"TotalCPU": 8,
|
||||
"TotalMemory": 25098706944,
|
||||
"UnhealthyContainerCount": 0,
|
||||
"VolumeCount": 10
|
||||
},
|
||||
"EndpointId": 1,
|
||||
"Kubernetes": null
|
||||
}
|
||||
],
|
||||
"sources": null,
|
||||
"ssl": {
|
||||
"certPath": "",
|
||||
"httpEnabled": true,
|
||||
"keyPath": "",
|
||||
"selfSigned": false
|
||||
},
|
||||
"stacks": [
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"DeploymentStartStatus": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker/alpine37-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 2,
|
||||
"Name": "alpine",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "s3fd604zdba7z13tbq2x6lyue",
|
||||
"Type": 1,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
},
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"DeploymentStartStatus": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 5,
|
||||
"Name": "redis",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "",
|
||||
"Type": 2,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
},
|
||||
{
|
||||
"AdditionalFiles": null,
|
||||
"AutoUpdate": null,
|
||||
"CreatedBy": "",
|
||||
"CreationDate": 0,
|
||||
"DeploymentStartStatus": 0,
|
||||
"EndpointId": 1,
|
||||
"EntryPoint": "docker-compose.yml",
|
||||
"Env": [],
|
||||
"FromAppTemplate": false,
|
||||
"GitConfig": null,
|
||||
"Id": 6,
|
||||
"Name": "nginx",
|
||||
"Namespace": "",
|
||||
"Option": null,
|
||||
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
|
||||
"ResourceControl": null,
|
||||
"Status": 1,
|
||||
"SwarmId": "",
|
||||
"Type": 2,
|
||||
"UpdateDate": 0,
|
||||
"UpdatedBy": ""
|
||||
}
|
||||
],
|
||||
"tags": null,
|
||||
"team_membership": null,
|
||||
"teams": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Name": "hello"
|
||||
}
|
||||
],
|
||||
"tunnel_server": {
|
||||
"PrivateKeySeed": ""
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"EndpointAuthorizations": null,
|
||||
"Id": 1,
|
||||
"Password": "$2a$10$siRDprr/5uUFAU8iom3Sr./WXQkN2dhSNjAC471pkJaALkghS762a",
|
||||
"PortainerAuthorizations": {
|
||||
"PortainerDockerHubInspect": true,
|
||||
"PortainerEndpointGroupList": true,
|
||||
"PortainerEndpointInspect": true,
|
||||
"PortainerEndpointList": true,
|
||||
"PortainerMOTD": true,
|
||||
"PortainerRegistryInspect": true,
|
||||
"PortainerRegistryList": true,
|
||||
"PortainerTeamList": true,
|
||||
"PortainerTemplateInspect": true,
|
||||
"PortainerTemplateList": true,
|
||||
"PortainerUserCreateToken": true,
|
||||
"PortainerUserInspect": true,
|
||||
"PortainerUserList": true,
|
||||
"PortainerUserListToken": true,
|
||||
"PortainerUserMemberships": true,
|
||||
"PortainerUserRevokeToken": true
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UseCache": false,
|
||||
"Username": "admin"
|
||||
},
|
||||
{
|
||||
"EndpointAuthorizations": null,
|
||||
"Id": 2,
|
||||
"Password": "$2a$10$WpCAW8mSt6FRRp1GkynbFOGSZnHR6E5j9cETZ8HiMlw06hVlDW/Li",
|
||||
"PortainerAuthorizations": {
|
||||
"PortainerDockerHubInspect": true,
|
||||
"PortainerEndpointGroupList": true,
|
||||
"PortainerEndpointInspect": true,
|
||||
"PortainerEndpointList": true,
|
||||
"PortainerMOTD": true,
|
||||
"PortainerRegistryInspect": true,
|
||||
"PortainerRegistryList": true,
|
||||
"PortainerTeamList": true,
|
||||
"PortainerTemplateInspect": true,
|
||||
"PortainerTemplateList": true,
|
||||
"PortainerUserCreateToken": true,
|
||||
"PortainerUserInspect": true,
|
||||
"PortainerUserList": true,
|
||||
"PortainerUserListToken": true,
|
||||
"PortainerUserMemberships": true,
|
||||
"PortainerUserRevokeToken": true
|
||||
},
|
||||
"Role": 1,
|
||||
"ThemeSettings": {
|
||||
"color": ""
|
||||
},
|
||||
"TokenIssueAt": 0,
|
||||
"UseCache": false,
|
||||
"Username": "prabhat"
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.43.0\",\"MigratorCount\":2,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null,
|
||||
"workflows": null
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (store *Store) GetConnection() portainer.Connection {
|
||||
return store.connection
|
||||
}
|
||||
|
||||
func MustNewTestStore(t testing.TB, init, secure bool) (bool, *Store) {
|
||||
newStore, store, teardown, err := NewTestStore(t, init, secure)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
t.Cleanup(teardown)
|
||||
|
||||
return newStore, store
|
||||
}
|
||||
|
||||
func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error) {
|
||||
// Creates unique temp directory in a concurrency friendly manner.
|
||||
storePath := t.TempDir()
|
||||
defaultKubectlShellImage := portainer.DefaultKubectlShellImage
|
||||
flags := &portainer.CLIFlags{
|
||||
KubectlShellImage: &defaultKubectlShellImage,
|
||||
}
|
||||
|
||||
fileService, err := filesystem.NewService(storePath, "")
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
|
||||
secretKey := []byte("apassphrasewhichneedstobe32bytes")
|
||||
if !secure {
|
||||
secretKey = nil
|
||||
}
|
||||
|
||||
connection, err := database.NewDatabase("boltdb", storePath, secretKey, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
store := NewStore(flags, fileService, connection)
|
||||
newStore, err := store.Open()
|
||||
if err != nil {
|
||||
return newStore, nil, nil, err
|
||||
}
|
||||
|
||||
if init {
|
||||
if err := store.Init(); err != nil {
|
||||
return newStore, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if newStore {
|
||||
// From MigrateData
|
||||
v := models.Version{
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
Edition: int(portainer.PortainerCE),
|
||||
}
|
||||
if err := store.VersionService.UpdateVersion(&v); err != nil {
|
||||
return newStore, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
teardown := func() {
|
||||
if err := store.Close(); err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
}
|
||||
|
||||
return newStore, store, teardown, nil
|
||||
}
|
||||
Reference in New Issue
Block a user