Compare commits
131 Commits
fix/EE-169
...
fix/EE-197
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29f528b6e9 | ||
|
|
c836fc9734 | ||
|
|
71de07bbea | ||
|
|
76ced401f0 | ||
|
|
33001a8654 | ||
|
|
f738af0f34 | ||
|
|
5c85c563e1 | ||
|
|
db00390cd2 | ||
|
|
32756f9e1b | ||
|
|
5ba80c3a44 | ||
|
|
77f73378ea | ||
|
|
734f077861 | ||
|
|
b5ec8c52fb | ||
|
|
988efe6b02 | ||
|
|
40a6645e23 | ||
|
|
cf60235696 | ||
|
|
65cc5342a7 | ||
|
|
90a18b5ded | ||
|
|
b29961e01e | ||
|
|
d17e7c8160 | ||
|
|
d3cc1a24cc | ||
|
|
fb7cdacbaa | ||
|
|
ec24826228 | ||
|
|
f0efc4f904 | ||
|
|
d18c8d0e88 | ||
|
|
4f350ab6f5 | ||
|
|
623079442f | ||
|
|
1ff5f25e40 | ||
|
|
ff87e687ec | ||
|
|
d4fd295c86 | ||
|
|
62f418836f | ||
|
|
ce5ea28727 | ||
|
|
00c7464c25 | ||
|
|
5eced421d5 | ||
|
|
006634e007 | ||
|
|
3cde10bcac | ||
|
|
9dcd5651e8 | ||
|
|
ba1f0f4018 | ||
|
|
41999e149f | ||
|
|
dfe0b3f69d | ||
|
|
588ce549ad | ||
|
|
edb25ee10d | ||
|
|
12e7aa6b60 | ||
|
|
f544d4447c | ||
|
|
158cdf596a | ||
|
|
3d6c6e2604 | ||
|
|
1ee363f8c9 | ||
|
|
109b27594a | ||
|
|
54d47ebc76 | ||
|
|
e6d690e31e | ||
|
|
6a67e8142d | ||
|
|
d93d88fead | ||
|
|
8383bc05c5 | ||
|
|
685552a661 | ||
|
|
1b0e58a4e8 | ||
|
|
0200a668df | ||
|
|
151dfe7e65 | ||
|
|
dcd1e902cd | ||
|
|
ed89587cb9 | ||
|
|
c93ec8d08c | ||
|
|
dad762de9f | ||
|
|
661931d8b0 | ||
|
|
b7841e7fc3 | ||
|
|
84e57cebc9 | ||
|
|
8096c5e8bc | ||
|
|
fd9427cd0b | ||
|
|
e60dbba93b | ||
|
|
551d287982 | ||
|
|
8421113d49 | ||
|
|
6bd72d21a8 | ||
|
|
fc4ff59bfd | ||
|
|
885ae16278 | ||
|
|
cd651f2cba | ||
|
|
328abfd74e | ||
|
|
fbcf67bc1e | ||
|
|
7fb2e44146 | ||
|
|
0cb5656db6 | ||
|
|
e4fd43e4fc | ||
|
|
34c2a16363 | ||
|
|
0f33e4ae99 | ||
|
|
75071dfade | ||
|
|
34f6e11f1d | ||
|
|
2ecc8ab5c9 | ||
|
|
fce885901f | ||
|
|
fe8f50512c | ||
|
|
e3b6e4a1d3 | ||
|
|
01529203f1 | ||
|
|
af98660a55 | ||
|
|
50f63ae865 | ||
|
|
7b72130433 | ||
|
|
7611cc415a | ||
|
|
9045e17cba | ||
|
|
46ffca92fd | ||
|
|
f0a88b7367 | ||
|
|
7437006359 | ||
|
|
9c80501738 | ||
|
|
377326085d | ||
|
|
03d34076d8 | ||
|
|
09cf4c1bbe | ||
|
|
9c279e7fae | ||
|
|
db04bc9f38 | ||
|
|
7d40a83d03 | ||
|
|
d4f581a596 | ||
|
|
5ad3cacefd | ||
|
|
6ac9c4367e | ||
|
|
8aa03bb81b | ||
|
|
d14c7b0309 | ||
|
|
a6138dd5a3 | ||
|
|
fc52830c7d | ||
|
|
6d510c4f30 | ||
|
|
e63732484a | ||
|
|
bcdc342cbd | ||
|
|
092d217985 | ||
|
|
0efbf5bbf3 | ||
|
|
48c762c98b | ||
|
|
4267304e50 | ||
|
|
24f11902b2 | ||
|
|
33118babdd | ||
|
|
483559af09 | ||
|
|
1796545d2e | ||
|
|
af8065e8c2 | ||
|
|
49d2c68a19 | ||
|
|
50393519ba | ||
|
|
16dc58a5f1 | ||
|
|
414f2c8c60 | ||
|
|
689c2193c0 | ||
|
|
9121e8e69c | ||
|
|
53a2205f06 | ||
|
|
a098e24cca | ||
|
|
5d8c23e3a6 | ||
|
|
52f9320952 |
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -30,7 +30,7 @@ A clear and concise description of what you expected to happen.
|
||||
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
|
||||
142
api/bolt/backup.go
Normal file
142
api/bolt/backup.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
)
|
||||
|
||||
var backupDefaults = struct {
|
||||
backupDir string
|
||||
commonDir string
|
||||
databaseFileName string
|
||||
}{
|
||||
"backups",
|
||||
"common",
|
||||
databaseFileName,
|
||||
}
|
||||
|
||||
var backupLog = plog.NewScopedLog("bolt, backup")
|
||||
|
||||
//
|
||||
// Backup Helpers
|
||||
//
|
||||
|
||||
// createBackupFolders create initial folders for backups
|
||||
func (store *Store) createBackupFolders() {
|
||||
// create common dir
|
||||
commonDir := store.commonBackupDir()
|
||||
if exists, _ := store.fileService.FileExists(commonDir); !exists {
|
||||
if err := os.MkdirAll(commonDir, 0700); err != nil {
|
||||
backupLog.Error("Error while creating common backup folder", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) databasePath() string {
|
||||
return path.Join(store.path, databaseFileName)
|
||||
}
|
||||
|
||||
func (store *Store) commonBackupDir() string {
|
||||
return path.Join(store.path, backupDefaults.backupDir, backupDefaults.commonDir)
|
||||
}
|
||||
|
||||
func (store *Store) copyDBFile(from string, to string) error {
|
||||
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
|
||||
err := store.fileService.Copy(from, to, true)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BackupOptions provide a helper to inject backup options
|
||||
type BackupOptions struct {
|
||||
Version int
|
||||
BackupDir string
|
||||
BackupFileName string
|
||||
BackupPath string
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
}
|
||||
if options.Version == 0 {
|
||||
options.Version, _ = store.version()
|
||||
}
|
||||
if options.BackupDir == "" {
|
||||
options.BackupDir = store.commonBackupDir()
|
||||
}
|
||||
if options.BackupFileName == "" {
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
|
||||
}
|
||||
if options.BackupPath == "" {
|
||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// BackupWithOptions backup current database with options
|
||||
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
|
||||
backupLog.Info("creating db backup")
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
// Restore strategies:
|
||||
// - default: restore latest from current edition
|
||||
// - restore a specific
|
||||
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
|
||||
options = store.setupOptions(options)
|
||||
|
||||
// Check if backup file exist before restoring
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
backupLog.Error("Error while closing store before restore", err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info("Restoring db backup")
|
||||
err = store.copyDBFile(options.BackupPath, store.databasePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Open()
|
||||
}
|
||||
|
||||
// RemoveWithOptions removes backup database based on supplied options
|
||||
func (store *Store) RemoveWithOptions(options *BackupOptions) error {
|
||||
backupLog.Info("Removing db backup")
|
||||
|
||||
options = store.setupOptions(options)
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
|
||||
err = os.Remove(options.BackupPath)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
116
api/bolt/backup_test.go
Normal file
116
api/bolt/backup_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// 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 TestCreateBackupFolders(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
backupPath := path.Join(store.path, backupDefaults.backupDir)
|
||||
|
||||
if isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to not exist")
|
||||
}
|
||||
|
||||
store.createBackupFolders()
|
||||
if !isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
if store == nil {
|
||||
t.Error("Expect to create a store")
|
||||
}
|
||||
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
t.Error("Expect to get CE Edition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("Backup should create default db backup", func(t *testing.T) {
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
store.BackupWithOptions(nil)
|
||||
|
||||
backupFileName := path.Join(store.path, "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
|
||||
store.BackupWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
BackupDir: store.commonBackupDir(),
|
||||
})
|
||||
backupFileName := path.Join(store.path, "backups", "common", beforePortainerVersionUpgradeBackup)
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveWithOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("successfully removes file if existent", func(t *testing.T) {
|
||||
store.createBackupFolders()
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
filePath := path.Join(options.BackupDir, options.BackupFileName)
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("file should be created; err=%s", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
err = store.RemoveWithOptions(options)
|
||||
if err != nil {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
|
||||
}
|
||||
|
||||
if isFileExist(f.Name()) {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails to removes file if non-existent", func(t *testing.T) {
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
err := store.RemoveWithOptions(options)
|
||||
if err == nil {
|
||||
t.Error("RemoveWithOptions should fail for non-existent file")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package bolt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
@@ -21,7 +20,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -36,7 +34,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
"github.com/portainer/portainer/api/bolt/webhook"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -76,6 +73,14 @@ type Store struct {
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) version() (int, error) {
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
@@ -85,25 +90,13 @@ func (store *Store) edition() portainer.SoftwareEdition {
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
|
||||
store := &Store{
|
||||
func NewStore(storePath string, fileService portainer.FileService) *Store {
|
||||
return &Store{
|
||||
path: storePath,
|
||||
fileService: fileService,
|
||||
isNew: true,
|
||||
connection: &internal.DbConnection{},
|
||||
}
|
||||
|
||||
databasePath := path.Join(storePath, databaseFileName)
|
||||
databaseFileExists, err := fileService.FileExists(databasePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if databaseFileExists {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
@@ -115,7 +108,17 @@ func (store *Store) Open() error {
|
||||
}
|
||||
store.connection.DB = db
|
||||
|
||||
return store.initServices()
|
||||
err = store.initServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we have DBVersion in the database then ensure we flag this as NOT a new store
|
||||
if _, err := store.VersionService.DBVersion(); err == nil {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
@@ -133,64 +136,6 @@ func (store *Store) IsNew() bool {
|
||||
return store.isNew
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
log.Printf("An error occurred during database migration: %s\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -199,3 +144,11 @@ func (store *Store) BackupTo(w io.Writer) error {
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func (store *Store) Init() error {
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
KubectlShellImage: portainer.DefaultKubectlShellImage,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
149
api/bolt/migrate_data.go
Normal file
149
api/bolt/migrate_data.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
store.Rollback(true)
|
||||
err = fmt.Errorf("%v", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// !Important: we must use a named return value in the function definition and not a local
|
||||
// !variable referenced from the closure or else the return value will be incorrectly set
|
||||
return migrator.Migrate()
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
migrator, err := store.newMigrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// backup db file before upgrading DB to support rollback
|
||||
isUpdating, err := store.VersionService.IsUpdating()
|
||||
if err != nil && err != errors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isUpdating && migrator.Version() != portainer.DBVersion {
|
||||
err = store.backupVersion(migrator)
|
||||
if err != nil {
|
||||
return werrors.Wrapf(err, "failed to backup database")
|
||||
}
|
||||
}
|
||||
|
||||
if migrator.Version() < portainer.DBVersion {
|
||||
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
|
||||
err = store.FailSafeMigrate(migrator)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database migration", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) newMigrator() (*migrator.Migrator, error) {
|
||||
version, err := store.version()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
return migrator.NewMigrator(migratorParams), nil
|
||||
}
|
||||
|
||||
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
|
||||
// - db backup prior to version upgrade
|
||||
// - db rollback
|
||||
func getBackupRestoreOptions(store *Store) *BackupOptions {
|
||||
return &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
}
|
||||
}
|
||||
|
||||
// backupVersion will backup the database or panic if any errors occur
|
||||
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
|
||||
migrateLog.Info("Backing up database prior to version upgrade...")
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
_, err := store.BackupWithOptions(options)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database backup", err)
|
||||
removalErr := store.RemoveWithOptions(options)
|
||||
if removalErr != nil {
|
||||
migrateLog.Error("An error occurred during store removal prior to backup", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
|
||||
if !force {
|
||||
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
|
||||
if err != nil || !confirmed {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
err := store.RestoreWithOptions(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Close()
|
||||
}
|
||||
172
api/bolt/migrate_data_test.go
Normal file
172
api/bolt/migrate_data_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
if v, _ := store.version(); v != versionWant {
|
||||
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
if !store.IsNew() {
|
||||
t.Error("Expect a new DB")
|
||||
}
|
||||
|
||||
store.MigrateData(false)
|
||||
|
||||
testVersion(store, portainer.DBVersion, t)
|
||||
store.Close()
|
||||
|
||||
store.Open()
|
||||
if store.IsNew() {
|
||||
t.Error("Expect store to NOT be new DB")
|
||||
}
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
version int
|
||||
expectedVersion int
|
||||
}{
|
||||
{version: 2, expectedVersion: portainer.DBVersion},
|
||||
{version: 21, expectedVersion: portainer.DBVersion},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
// Setup data
|
||||
store.VersionService.StoreDBVersion(tc.version)
|
||||
|
||||
// Required roles by migrations 22.2
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 1})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 2})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 3})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 4})
|
||||
|
||||
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.MigrateData(true)
|
||||
testVersion(store, tc.expectedVersion, t)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.Rollback(true)
|
||||
store.Open()
|
||||
testVersion(store, tc.version, t)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
version := 2
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(0)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if !isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.VersionService.StoreIsUpdating(true)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
wantDir := store.commonBackupDir()
|
||||
if !strings.HasSuffix(options.BackupDir, wantDir) {
|
||||
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
|
||||
}
|
||||
|
||||
wantFilename := "portainer.db.bak"
|
||||
if options.BackupFileName != wantFilename {
|
||||
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := 21
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
_, err := store.BackupWithOptions(getBackupRestoreOptions(store))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Change the current edition
|
||||
err = store.VersionService.StoreDBVersion(version + 10)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Rollback(true)
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
store.Open()
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
}
|
||||
327
api/bolt/migrator/migrate_ce.go
Normal file
327
api/bolt/migrator/migrate_ce.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return werrors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// set DB to updating status
|
||||
err := m.versionService.StoreIsUpdating(true)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreIsUpdating")
|
||||
}
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateAdminUserToDBVersion1")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion2")
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion2")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion3")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion4")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion5")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion6")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion7")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion8")
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion9")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion10")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion11")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToVersion12")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion13")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion14")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion15")
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTemplatesToVersion15")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion16")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateExtensionsToDBVersion17")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateRegistriesToDBVersion18")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion19")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSchedulesToDBVersion20")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion22")
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersAndRolesToDBVersion22")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTagsToDBVersion23")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB25")
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointSettingsToDB25")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStackResourceControlToDB27")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB30")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB32")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.1
|
||||
if m.currentDBVersion < 33 {
|
||||
err := m.migrateDBVersionToDB33()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB33")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.10
|
||||
if m.currentDBVersion < 34 {
|
||||
if err := m.migrateDBVersionToDB34(); err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB34")
|
||||
}
|
||||
}
|
||||
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
|
||||
|
||||
// reset DB updating status
|
||||
return m.versionService.StoreIsUpdating(false)
|
||||
}
|
||||
@@ -176,7 +176,8 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching environment docker id: %w", err)
|
||||
log.Printf("[WARN] [bolt,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
|
||||
@@ -213,7 +214,11 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
volumes := volumesData["Volumes"].([]interface{})
|
||||
for _, volumeMeta := range volumes {
|
||||
volume := volumeMeta.(map[string]interface{})
|
||||
volumeName := volume["Name"].(string)
|
||||
volumeName, nameExist := volume["Name"].(string)
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
||||
|
||||
21
api/bolt/migrator/migrate_dbversion32.go
Normal file
21
api/bolt/migrator/migrate_dbversion32.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB33() error {
|
||||
if err := m.migrateSettingsToDB33(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateSettingsToDB33() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionTo33() error {
|
||||
func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
err := migrateStackEntryPoint(m.stackService)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-34.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
assert.NoError(t, err, "failed to init testing DB connection")
|
||||
defer dbConn.Close()
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
db *bolt.DB
|
||||
currentDBVersion int
|
||||
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
@@ -97,295 +98,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 33 {
|
||||
if err := m.migrateDBVersionTo33(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
// Version exposes version of database
|
||||
func (migrator *Migrator) Version() int {
|
||||
return migrator.currentDBVersion
|
||||
}
|
||||
|
||||
@@ -192,8 +192,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
var stack portainer.Stack
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
stack := portainer.Stack{}
|
||||
err := internal.UnmarshalObject(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,18 +4,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/bolttest"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newGuidString(t *testing.T) string {
|
||||
@@ -35,7 +29,7 @@ func TestService_StackByWebhookID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
b := stackBuilder{t: t, store: store}
|
||||
@@ -93,7 +87,7 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package bolttest
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -6,13 +6,12 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
var errTempDir = errors.New("can't create a temp dir")
|
||||
|
||||
func MustNewTestStore(init bool) (*bolt.Store, func()) {
|
||||
func MustNewTestStore(init bool) (*Store, func()) {
|
||||
store, teardown, err := NewTestStore(init)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errTempDir) {
|
||||
@@ -24,7 +23,7 @@ func MustNewTestStore(init bool) (*bolt.Store, func()) {
|
||||
return store, teardown
|
||||
}
|
||||
|
||||
func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
func NewTestStore(init bool) (*Store, func(), error) {
|
||||
// Creates unique temp directory in a concurrency friendly manner.
|
||||
dataStorePath, err := ioutil.TempDir("", "boltdb")
|
||||
if err != nil {
|
||||
@@ -36,11 +35,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store := NewStore(dataStorePath, fileService)
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -60,7 +55,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
return store, teardown, nil
|
||||
}
|
||||
|
||||
func teardown(store *bolt.Store, dataStorePath string) {
|
||||
func teardown(store *Store, dataStorePath string) {
|
||||
err := store.Close()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
@@ -15,6 +15,7 @@ const (
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
updatingKey = "DB_UPDATING"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -83,6 +84,21 @@ func (service *Service) StoreDBVersion(version int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// IsUpdating retrieves the database updating status.
|
||||
func (service *Service) IsUpdating() (bool, error) {
|
||||
isUpdating, err := service.getKey(updatingKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return strconv.ParseBool(string(isUpdating))
|
||||
}
|
||||
|
||||
// StoreIsUpdating store the database updating status.
|
||||
func (service *Service) StoreIsUpdating(isUpdating bool) error {
|
||||
return service.setKey(updatingKey, strconv.FormatBool(isUpdating))
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
var data []byte
|
||||
|
||||
@@ -3,6 +3,7 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -32,6 +33,7 @@ type Service struct {
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
@@ -215,18 +217,13 @@ func (service *Service) checkTunnels() {
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnel.Jobs) > 0 {
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
} else {
|
||||
service.tunnelDetailsMap.Remove(item.Key)
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,12 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
// update the LastActivity
|
||||
service.SetTunnelStatusToActive(endpoint.ID)
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
@@ -74,15 +80,24 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
waitForAgentToConnect := 2 * time.Duration(endpoint.EdgeCheckinInterval)
|
||||
|
||||
for waitForAgentToConnect >= 0 {
|
||||
waitForAgentToConnect--
|
||||
time.Sleep(time.Second)
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
|
||||
// It sets the status to ACTIVE.
|
||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
@@ -112,6 +127,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
|
||||
@@ -47,6 +47,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
|
||||
24
api/cli/confirm.go
Normal file
24
api/cli/confirm.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Confirm starts a rollback db cli application
|
||||
func Confirm(message string) (bool, error) {
|
||||
log.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
@@ -56,17 +56,24 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||
return fileService
|
||||
}
|
||||
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store := bolt.NewStore(dataStorePath, fileService)
|
||||
err := store.Open()
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
}
|
||||
|
||||
if rollback {
|
||||
err := store.Rollback(false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed rolling back: %s", err)
|
||||
}
|
||||
|
||||
log.Println("Exiting rollback")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing data store: %v", err)
|
||||
@@ -399,7 +406,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
|
||||
dataStore := initDataStore(*flags.Data, *flags.Rollback, fileService, shutdownCtx)
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -460,6 +467,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
|
||||
@@ -91,7 +91,11 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
|
||||
5
api/exec/common.go
Normal file
5
api/exec/common.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package exec
|
||||
|
||||
import "regexp"
|
||||
|
||||
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -81,8 +80,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
|
||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
|
||||
23
api/exec/exectest/kubernetes_mocks.go
Normal file
23
api/exec/exectest/kubernetes_mocks.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package exectest
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type kubernetesMockDeployer struct{}
|
||||
|
||||
func NewKubernetesDeployer() portainer.KubernetesDeployer {
|
||||
return &kubernetesMockDeployer{}
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
@@ -96,9 +95,9 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--token", token,
|
||||
"--namespace", namespace,
|
||||
args := []string{"--token", token}
|
||||
if namespace != "" {
|
||||
args = append(args, "--namespace", namespace)
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -45,19 +44,26 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -65,7 +71,10 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
|
||||
@@ -85,7 +94,10 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -109,7 +121,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
@@ -122,7 +134,10 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
@@ -142,7 +157,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
}
|
||||
}
|
||||
|
||||
return command, args
|
||||
return command, args, nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
@@ -190,8 +205,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func configureFilePaths(args []string, filePaths []string) []string {
|
||||
|
||||
43
api/go.mod
43
api/go.mod
@@ -3,57 +3,48 @@ module github.com/portainer/portainer/api
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210913052337-365741c1c320
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.3
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
)
|
||||
|
||||
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
|
||||
|
||||
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456
|
||||
|
||||
975
api/go.sum
975
api/go.sum
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id AuthenticateUser
|
||||
// @summary Authenticate
|
||||
// @description **Access policy**: public
|
||||
// @description Use this environment(endpoint) to authenticate against Portainer using a username and password.
|
||||
// @tags auth
|
||||
// @accept json
|
||||
|
||||
@@ -44,6 +44,7 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
|
||||
|
||||
// @id ValidateOAuth
|
||||
// @summary Authenticate with OAuth
|
||||
// @description **Access policy**: public
|
||||
// @tags auth
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: authenticated
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
// @success 204 "Success"
|
||||
|
||||
@@ -27,8 +27,9 @@ func (p *backupPayload) Validate(r *http.Request) error {
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce octet-stream
|
||||
// @param Password body string false "Password to encrypt the backup with"
|
||||
// @param body body backupPayload false "An object contains the password to encrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
|
||||
@@ -22,10 +22,9 @@ type restorePayload struct {
|
||||
// @description Triggers a system restore using provided backup file
|
||||
// @description **Access policy**: public
|
||||
// @tags backup
|
||||
// @param FileContent body []byte true "Content of the backup"
|
||||
// @param FileName body string true "File name"
|
||||
// @param Password body string false "Password to decrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @accept json
|
||||
// @param restorePayload body restorePayload true "Restore request payload"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /restore [post]
|
||||
|
||||
@@ -3,6 +3,7 @@ package customtemplates
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
@@ -129,9 +130,20 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidNote(note string) bool {
|
||||
if govalidator.IsNull(note) {
|
||||
return true
|
||||
}
|
||||
match, _ := regexp.MatchString("<img", note)
|
||||
return !match
|
||||
}
|
||||
|
||||
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
|
||||
var payload customTemplateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
@@ -218,6 +230,9 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -285,6 +300,9 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||
payload.Logo = logo
|
||||
|
||||
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
||||
if !isValidNote(note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
payload.Note = note
|
||||
|
||||
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// @id CustomTemplateDelete
|
||||
// @summary Remove a template
|
||||
// @description Remove a template.
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @param id path int true "Template identifier"
|
||||
|
||||
@@ -18,7 +18,7 @@ type fileResponse struct {
|
||||
// @id CustomTemplateFile
|
||||
// @summary Get Template stack file content.
|
||||
// @description Retrieve the content of the Stack file for the specified custom template
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @produce json
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Template identifier"
|
||||
// @success 200 {object} portainer.CustomTemplate "Success"
|
||||
|
||||
@@ -51,6 +51,9 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
return errors.New("Invalid custom template description")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeGroupCreate
|
||||
// @summary Create an EdgeGroup
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -13,11 +13,9 @@ import (
|
||||
|
||||
// @id EdgeGroupDelete
|
||||
// @summary Deletes an EdgeGroup
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 204
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
|
||||
@@ -12,10 +12,9 @@ import (
|
||||
|
||||
// @id EdgeGroupInspect
|
||||
// @summary Inspects an EdgeGroup
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
|
||||
@@ -17,10 +17,9 @@ type decoratedEdgeGroup struct {
|
||||
|
||||
// @id EdgeGroupList
|
||||
// @summary list EdgeGroups
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups"
|
||||
// @failure 500
|
||||
|
||||
@@ -36,7 +36,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EgeGroupUpdate
|
||||
// @summary Updates an EdgeGroup
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -16,10 +16,9 @@ import (
|
||||
|
||||
// @id EdgeJobCreate
|
||||
// @summary Create an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file, string)
|
||||
// @param body_string body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
|
||||
|
||||
@@ -13,11 +13,9 @@ import (
|
||||
|
||||
// @id EdgeJobDelete
|
||||
// @summary Delete an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
|
||||
@@ -16,10 +16,9 @@ type edgeJobFileResponse struct {
|
||||
|
||||
// @id EdgeJobFile
|
||||
// @summary Fetch a file of an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} edgeJobFileResponse
|
||||
|
||||
@@ -17,10 +17,9 @@ type edgeJobInspectResponse struct {
|
||||
|
||||
// @id EdgeJobInspect
|
||||
// @summary Inspect an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
|
||||
// @id EdgeJobList
|
||||
// @summary Fetch EdgeJobs list
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeJob
|
||||
// @failure 500
|
||||
|
||||
@@ -13,10 +13,9 @@ import (
|
||||
|
||||
// @id EdgeJobTasksClear
|
||||
// @summary Clear the log for a specifc task on an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -12,10 +12,9 @@ import (
|
||||
|
||||
// @id EdgeJobTasksCollect
|
||||
// @summary Collect the log for a specifc task on an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -15,10 +15,9 @@ type fileResponse struct {
|
||||
|
||||
// @id EdgeJobTaskLogsInspect
|
||||
// @summary Fetch the log for a specifc task on an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -19,10 +19,9 @@ type taskContainer struct {
|
||||
|
||||
// @id EdgeJobTasksList
|
||||
// @summary Fetch the list of tasks on an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {array} taskContainer
|
||||
|
||||
@@ -30,7 +30,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeJobUpdate
|
||||
// @summary Update an EdgeJob
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -19,10 +19,9 @@ import (
|
||||
|
||||
// @id EdgeStackCreate
|
||||
// @summary Create an EdgeStack
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file,string,repository)
|
||||
// @param body_string body swarmStackFromFileContentPayload true "Required when using method=string"
|
||||
|
||||
@@ -13,11 +13,9 @@ import (
|
||||
|
||||
// @id EdgeStackDelete
|
||||
// @summary Delete an EdgeStack
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
|
||||
@@ -17,10 +17,9 @@ type stackFileResponse struct {
|
||||
|
||||
// @id EdgeStackFile
|
||||
// @summary Fetches the stack file for an EdgeStack
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} stackFileResponse
|
||||
|
||||
@@ -12,10 +12,9 @@ import (
|
||||
|
||||
// @id EdgeStackInspect
|
||||
// @summary Inspect an EdgeStack
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
|
||||
// @id EdgeStackList
|
||||
// @summary Fetches the list of EdgeStacks
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeStack
|
||||
// @failure 500
|
||||
|
||||
@@ -33,7 +33,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeStackUpdate
|
||||
// @summary Update an EdgeStack
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -17,7 +17,7 @@ type templateFileFormat struct {
|
||||
|
||||
// @id EdgeTemplateList
|
||||
// @summary Fetches the list of Edge Templates
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_templates
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -21,7 +21,7 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
|
||||
// endpointEdgeJobsLogs
|
||||
// @summary Inspect an EdgeJob Log
|
||||
// @description
|
||||
// @description **Access policy**: public
|
||||
// @tags edge, endpoints
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -19,7 +19,7 @@ type configResponse struct {
|
||||
}
|
||||
|
||||
// @summary Inspect an Edge Stack for an Environment(Endpoint)
|
||||
// @description
|
||||
// @description **Access policy**: public
|
||||
// @tags edge, endpoints, edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
|
||||
@@ -2,14 +2,12 @@ package endpointproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /api/endpoints/{id}/association [put]
|
||||
// @router /endpoints/{id}/association [put]
|
||||
func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,11 +18,27 @@ import (
|
||||
)
|
||||
|
||||
type dockerhubStatusResponse struct {
|
||||
// Remaiming images to pull
|
||||
Remaining int `json:"remaining"`
|
||||
Limit int `json:"limit"`
|
||||
// Daily limit
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// GET request on /api/endpoints/{id}/dockerhub/{registryId}
|
||||
// @id endpointDockerhubStatus
|
||||
// @summary fetch docker pull limits
|
||||
// @description get docker pull limits for a docker hub registry in the environment
|
||||
// @description **Access policy**:
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "endpoint ID"
|
||||
// @param registryId path int true "registry ID"
|
||||
// @success 200 {object} dockerhubStatusResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "registry or endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/dockerhub/{registryId} [get]
|
||||
func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -29,6 +29,12 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id endpointExtensionAdd
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions [post]
|
||||
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,13 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// @id endpointExtensionRemove
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param extensionType path string true "Extension Type"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions/{extensionType} [delete]
|
||||
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,7 +12,20 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// GET request on /endpoints/{id}/registries/{registryId}
|
||||
// @id endpointRegistryInspect
|
||||
// @summary get registry for environment
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "identifier"
|
||||
// @param registryId path int true "Registry identifier"
|
||||
// @success 200 {object} portainer.Registry "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Registry not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries/{registryId} [get]
|
||||
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,18 @@ import (
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
// GET request on /endpoints/{id}/registries?namespace
|
||||
// @id endpointRegistriesList
|
||||
// @summary List Registries on environment
|
||||
// @description List all registries based on the current user authorizations in current environment.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @param namespace query string false "required if kubernetes environment, will show registries by namespace"
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 {array} portainer.Registry "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries [get]
|
||||
func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,7 +21,22 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /endpoints/{id}/registries/{registryId}
|
||||
// @id endpointRegistryAccess
|
||||
// @summary update registry access for environment
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param registryId path int true "Registry identifier"
|
||||
// @param body body registryAccessPayload true "details"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries/{registryId} [put]
|
||||
func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -36,9 +36,9 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
|
||||
// @id EndpointSettingsUpdate
|
||||
// @summary Update settings for an environments(endpoints)
|
||||
// @description Update settings for an environments(endpoints).
|
||||
// @description **Access policy**: administrator
|
||||
// @summary Update settings for an environment(endpoint)
|
||||
// @description Update settings for an environment(endpoint).
|
||||
// @description **Access policy**: authenticated
|
||||
// @security jwt
|
||||
// @tags endpoints
|
||||
// @accept json
|
||||
@@ -49,7 +49,7 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /api/endpoints/{id}/settings [put]
|
||||
// @router /endpoints/{id}/settings [put]
|
||||
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
// @id EndpointSnapshot
|
||||
// @summary Snapshots an environments(endpoints)
|
||||
// @description Snapshots an environments(endpoints)
|
||||
// @description **Access policy**: restricted
|
||||
// @summary Snapshots an environment(endpoint)
|
||||
// @description Snapshots an environment(endpoint)
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
|
||||
@@ -56,7 +56,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||
// @id EndpointUpdate
|
||||
// @summary Update an environment(endpoint)
|
||||
// @description Update an environment(endpoint).
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: authenticated
|
||||
// @security jwt
|
||||
// @tags endpoints
|
||||
// @accept json
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
@@ -53,6 +54,7 @@ type Handler struct {
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
LDAPHandler *ldap.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
@@ -72,7 +74,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.9.0
|
||||
// @version 2.9.2
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -189,6 +191,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ldap"):
|
||||
http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
|
||||
@@ -27,15 +27,17 @@ type Handler struct {
|
||||
requestBouncer requestBouncer
|
||||
dataStore portainer.DataStore
|
||||
kubeConfigService kubernetes.KubeConfigService
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
helmPackageManager libhelm.HelmPackageManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
dataStore: dataStore,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
helmPackageManager: helmPackageManager,
|
||||
kubeConfigService: kubeConfigService,
|
||||
}
|
||||
|
||||
@@ -12,11 +12,9 @@ import (
|
||||
// @id HelmDelete
|
||||
// @summary Delete Helm Release
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param release path string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
@@ -29,9 +30,10 @@ func Test_helmDelete(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "Error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes/validation"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type installChartPayload struct {
|
||||
@@ -31,7 +36,7 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
|
||||
// @id HelmInstall
|
||||
// @summary Install Helm Chart
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
@@ -131,5 +136,98 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.updateHelmAppManifest(r, manifest, installOpts.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// applyPortainerLabelsToHelmAppManifest will patch all the resources deployed in the helm release manifest
|
||||
// with portainer specific labels. This is to mark the resources as managed by portainer - hence the helm apps
|
||||
// wont appear external in the portainer UI.
|
||||
func (handler *Handler) applyPortainerLabelsToHelmAppManifest(r *http.Request, installOpts options.InstallOptions, manifest string) ([]byte, error) {
|
||||
// Patch helm release by adding with portainer labels to all deployed resources
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to retrieve user details from authentication token")
|
||||
}
|
||||
user, err := handler.dataStore.User().User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to load user information from the database")
|
||||
}
|
||||
|
||||
appLabels := kubernetes.GetHelmAppLabels(installOpts.Name, user.Username)
|
||||
labeledManifest, err := kubernetes.AddAppLabels([]byte(manifest), appLabels)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to label helm release manifest")
|
||||
}
|
||||
|
||||
return labeledManifest, nil
|
||||
}
|
||||
|
||||
// updateHelmAppManifest will update the resources of helm release manifest with portainer labels using kubectl.
|
||||
// The resources of the manifest will be updated in parallel and individuallly since resources of a chart
|
||||
// can be deployed to different namespaces.
|
||||
// NOTE: These updates will need to be re-applied when upgrading the helm release
|
||||
func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte, namespace string) error {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to find an endpoint on request context")
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve user details from authentication token")
|
||||
}
|
||||
|
||||
// extract list of yaml resources from helm manifest
|
||||
yamlResources, err := kubernetes.ExtractDocuments(manifest, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to extract documents from helm release manifest")
|
||||
}
|
||||
|
||||
// deploy individual resources in parallel
|
||||
g := new(errgroup.Group)
|
||||
for _, resource := range yamlResources {
|
||||
resource := resource // https://golang.org/doc/faq#closures_and_goroutines
|
||||
g.Go(func() error {
|
||||
tmpfile, err := ioutil.TempFile("", "helm-manifest-*")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create a tmp helm manifest file")
|
||||
}
|
||||
defer func() {
|
||||
tmpfile.Close()
|
||||
os.Remove(tmpfile.Name())
|
||||
}()
|
||||
|
||||
if _, err := tmpfile.Write(resource); err != nil {
|
||||
return errors.Wrap(err, "failed to write a tmp helm manifest file")
|
||||
}
|
||||
|
||||
// get resource namespace, fallback to provided namespace if not explicit on resource
|
||||
resourceNamespace, err := kubernetes.GetNamespace(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resourceNamespace == "" {
|
||||
resourceNamespace = namespace
|
||||
}
|
||||
|
||||
_, err = handler.kubernetesDeployer.Deploy(tokenData.ID, endpoint, []string{tmpfile.Name()}, resourceNamespace)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != nil {
|
||||
return errors.Wrap(err, "unable to patch helm release using kubectl")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
@@ -31,9 +32,10 @@ func Test_helmInstall(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// @id HelmList
|
||||
// @summary List Helm Releases
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,10 @@ func Test_helmList(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// @id HelmRepoSearch
|
||||
// @summary Search Helm Charts
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @security jwt
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
// @id HelmShow
|
||||
// @summary Show Helm Chart Information
|
||||
// @description
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @param chart query string true "Chart name"
|
||||
// @param command path string true "chart/values/readme"
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @id GetKubernetesConfig
|
||||
// @summary Generates kubeconfig file enabling client communication with k8s api server
|
||||
// @description Generates kubeconfig file enabling client communication with k8s api server
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags kubernetes
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// @id getKubernetesNodesLimits
|
||||
// @summary Get CPU and memory limits of all nodes within k8s cluster
|
||||
// @description Get CPU and memory limits of all nodes within k8s cluster
|
||||
// @description **Access policy**: authorized
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags kubernetes
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
53
api/http/handler/ldap/handler.go
Normal file
53
api/http/handler/ldap/handler.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle LDAP search Operations
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
LDAPService portainer.LDAPService
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/ldap/check",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.ldapCheck))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) prefillSettings(ldapSettings *portainer.LDAPSettings) error {
|
||||
if !ldapSettings.AnonymousMode && ldapSettings.Password == "" {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapSettings.Password = settings.LDAPSettings.Password
|
||||
}
|
||||
|
||||
if (ldapSettings.TLSConfig.TLS || ldapSettings.StartTLS) && !ldapSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, err := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ldapSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package settings
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -7,42 +7,43 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
type settingsLDAPCheckPayload struct {
|
||||
type checkPayload struct {
|
||||
LDAPSettings portainer.LDAPSettings
|
||||
}
|
||||
|
||||
func (payload *settingsLDAPCheckPayload) Validate(r *http.Request) error {
|
||||
func (payload *checkPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id SettingsLDAPCheck
|
||||
// @id LDAPCheck
|
||||
// @summary Test LDAP connectivity
|
||||
// @description Test LDAP connectivity using LDAP details
|
||||
// @description **Access policy**: administrator
|
||||
// @tags settings
|
||||
// @tags ldap
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @param body body settingsLDAPCheckPayload true "details"
|
||||
// @param body body checkPayload true "details"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /settings/ldap/check [put]
|
||||
func (handler *Handler) settingsLDAPCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload settingsLDAPCheckPayload
|
||||
// @router /ldap/check [post]
|
||||
func (handler *Handler) ldapCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload checkPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
if (payload.LDAPSettings.TLSConfig.TLS || payload.LDAPSettings.StartTLS) && !payload.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
payload.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
settings := &payload.LDAPSettings
|
||||
|
||||
err = handler.prefillSettings(settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch default settings", err}
|
||||
}
|
||||
|
||||
err = handler.LDAPService.TestConnectivity(&payload.LDAPSettings)
|
||||
err = handler.LDAPService.TestConnectivity(settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to connect to LDAP server", err}
|
||||
}
|
||||
@@ -26,7 +26,9 @@ type motdData struct {
|
||||
Style string `json:"style"`
|
||||
}
|
||||
|
||||
// @id MOTD
|
||||
// @summary fetches the message of the day
|
||||
// @description **Access policy**: restricted
|
||||
// @tags motd
|
||||
// @security jwt
|
||||
// @produce json
|
||||
|
||||
@@ -81,7 +81,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||
// @id RegistryConfigure
|
||||
// @summary Configures a registry
|
||||
// @description Configures a registry.
|
||||
// @description **Access policy**: admin
|
||||
// @description **Access policy**: restricted
|
||||
// @tags registries
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -62,7 +62,7 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
|
||||
// @id RegistryCreate
|
||||
// @summary Create a new registry
|
||||
// @description Create a new registry.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: restricted
|
||||
// @tags registries
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// @id RegistryDelete
|
||||
// @summary Remove a registry
|
||||
// @description Remove a registry
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: restricted
|
||||
// @tags registries
|
||||
// @security jwt
|
||||
// @param id path int true "Registry identifier"
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// @id RegistryInspect
|
||||
// @summary Inspect a registry
|
||||
// @description Retrieve details about a registry.
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: restricted
|
||||
// @tags registries
|
||||
// @security jwt
|
||||
// @produce json
|
||||
|
||||
@@ -37,7 +37,7 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
||||
// @id RegistryUpdate
|
||||
// @summary Update a registry
|
||||
// @description Update a registry
|
||||
// @description **Access policy**: administrator
|
||||
// @description **Access policy**: restricted
|
||||
// @tags registries
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -38,7 +38,7 @@ func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
||||
// @id ResourceControlUpdate
|
||||
// @summary Update a resource control
|
||||
// @description Update a resource control
|
||||
// @description **Access policy**: restricted
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags resource_controls
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -35,8 +35,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/settings/public",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
|
||||
h.Handle("/settings/authentication/checkLDAP",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user