feat(backup): Add backup/restore to the server

pull/4965/head
Dmitry Salakhov 2021-04-06 22:08:43 +12:00
parent c04bbb5775
commit a3ec2f8e85
65 changed files with 2394 additions and 564 deletions

View File

@ -0,0 +1,69 @@
package adminmonitor
import (
"context"
"log"
"time"
portainer "github.com/portainer/portainer/api"
)
var logFatalf = log.Fatalf
type Monitor struct {
timeout time.Duration
datastore portainer.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore portainer.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
logFatalf("%s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
case <-m.shutdownCtx.Done():
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
}
}()
}
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
// WasInitialized is a system initialization check
func (m *Monitor) WasInitialized() (bool, error) {
users, err := m.datastore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}

View File

@ -0,0 +1,50 @@
package adminmonitor
import (
"context"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_stopWithoutStarting(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
}
func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
monitor.Stop()
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor")
monitor.Stop()
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
}

119
api/archive/targz.go Normal file
View File

@ -0,0 +1,119 @@
package archive
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// TarGzDir creates a tar.gz archive and returns it's path.
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
outFile, err := os.Create(targzPath)
if err != nil {
return "", err
}
defer outFile.Close()
zipWriter := gzip.NewWriter(outFile)
defer zipWriter.Close()
tarWriter := tar.NewWriter(zipWriter)
defer tarWriter.Close()
err = filepath.Walk(absolutePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == targzPath {
return nil // skip archive file
}
pathInArchive := filepath.Clean(strings.TrimPrefix(path, absolutePath))
if pathInArchive == "" {
return nil // skip root dir
}
return addToArchive(tarWriter, pathInArchive, path, info)
})
return targzPath, err
}
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = pathInArchive // use relative paths in archive
err = tarWriter.WriteHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(tarWriter, file)
return err
}
// ExtractTarGz reads a .tar.gz archive from the reader and extracts it into outputDirPath directory
func ExtractTarGz(r io.Reader, outputDirPath string) error {
zipReader, err := gzip.NewReader(r)
if err != nil {
return err
}
defer zipReader.Close()
tarReader := tar.NewReader(zipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch header.Typeflag {
case tar.TypeDir:
// skip, dir will be created with a file
case tar.TypeReg:
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
if err := os.MkdirAll(filepath.Dir(p), 0744); err != nil {
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
}
outFile, err := os.Create(p)
if err != nil {
return fmt.Errorf("Failed to create file %s", header.Name)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("Failed to extract file %s", header.Name)
}
outFile.Close()
default:
return fmt.Errorf("Tar: uknown type: %v in %s",
header.Typeflag,
header.Name)
}
}
return nil
}

98
api/archive/targz_test.go Normal file
View File

@ -0,0 +1,98 @@
package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := os.MkdirTemp("", "extract")
defer os.RemoveAll(extractionDir)
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := os.MkdirTemp("", "extract")
defer os.RemoveAll(extractionDir)
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
wasExtracted("outer")
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}

84
api/backup/backup.go Normal file
View File

@ -0,0 +1,84 @@
package backup
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
)
const rwxr__r__ fs.FileMode = 0744
var filesToBackup = []string{"compose", "config.json", "custom_templates", "edge_jobs", "edge_stacks", "extensions", "portainer.key", "portainer.pub", "tls"}
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := copyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
archivePath, err := archive.TarGzDir(backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to make an archive")
}
if password != "" {
archivePath, err = encrypt(archivePath, password)
if err != nil {
return "", errors.Wrap(err, "Failed to encrypt backup with the password")
}
}
return archivePath, nil
}
func backupDb(backupDirPath string, datastore portainer.DataStore) error {
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
if err != nil {
return err
}
if err = datastore.BackupTo(backupWriter); err != nil {
return err
}
return backupWriter.Close()
}
func encrypt(path string, passphrase string) (string, error) {
in, err := os.Open(path)
if err != nil {
return "", err
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
out, err := os.Create(outFileName)
if err != nil {
return "", err
}
err = crypto.AesEncrypt(in, out, []byte(passphrase))
return outFileName, err
}

68
api/backup/copy.go Normal file
View File

@ -0,0 +1,68 @@
package backup
import (
"errors"
"io"
"os"
"path/filepath"
"strings"
)
func copyPath(path string, toDir string) error {
info, err := os.Stat(path)
if err != nil && errors.Is(err, os.ErrNotExist) {
// skip copy if file does not exist
return nil
}
if !info.IsDir() {
destination := filepath.Join(toDir, info.Name())
return copyFile(path, destination)
}
return copyDir(path, toDir)
}
func copyDir(fromDir, toDir string) error {
cleanedSourcePath := filepath.Clean(fromDir)
parentDirectory := filepath.Dir(cleanedSourcePath)
err := filepath.Walk(cleanedSourcePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
destination := filepath.Join(toDir, strings.TrimPrefix(path, parentDirectory))
if info.IsDir() {
return nil // skip directory creations
}
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
return nil // don't copy symlinks
}
return copyFile(path, destination)
})
return err
}
// copies regular a file from src to dst
func copyFile(src, dst string) error {
from, err := os.Open(src)
if err != nil {
return err
}
defer from.Close()
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return err
}
to, err := os.Create(dst)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
return err
}

104
api/backup/copy_test.go Normal file
View File

@ -0,0 +1,104 @@
package backup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
err := copyFile("does-not-exist", tmpdir)
assert.NotNil(t, err)
}
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.Nil(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
func Test_copyDir_shouldCopyAllFilesAndDirectories(t *testing.T) {
destination, _ := os.MkdirTemp("", "destination")
defer os.RemoveAll(destination)
err := copyDir("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func Test_backupPath_shouldSkipWhenNotExist(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
err := copyPath("does-not-exists", tmpdir)
assert.Nil(t, err)
assert.Empty(t, listFiles(tmpdir))
}
func Test_backupPath_shouldCopyFile(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := copyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.Nil(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.Nil(t, err)
assert.Equal(t, content, copyContent)
}
func Test_backupPath_shouldCopyDir(t *testing.T) {
destination, _ := os.MkdirTemp("", "destination")
defer os.RemoveAll(destination)
err := copyPath("./test_assets/copy_test", destination)
assert.Nil(t, err)
createdFiles := listFiles(destination)
contains(t, createdFiles, filepath.Join(destination, "copy_test", "outer"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
contains(t, createdFiles, filepath.Join(destination, "copy_test", "dir", "inner"))
}

68
api/backup/restore.go Normal file
View File

@ -0,0 +1,68 @@
package backup
import (
"context"
"io"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
)
var filesToRestore = append(filesToBackup, "portainer.db")
// Restores system state from backup archive, will trigger system shutdown, when finished.
func RestoreArchive(archive io.Reader, password string, filestorePath string, gate *offlinegate.OfflineGate, datastore portainer.DataStore, shutdownTrigger context.CancelFunc) error {
var err error
if password != "" {
archive, err = decrypt(archive, password)
if err != nil {
return errors.Wrap(err, "failed to decrypt the archive")
}
}
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer os.RemoveAll(filepath.Dir(restorePath))
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
unlock := gate.Lock()
defer unlock()
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
shutdownTrigger()
return nil
}
func decrypt(r io.Reader, password string) (io.Reader, error) {
return crypto.AesDecrypt(r, []byte(password))
}
func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := copyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -2,7 +2,7 @@ package customtemplate
import ( import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
) )
@ -13,18 +13,18 @@ const (
// Service represents a service for managing custom template data. // Service represents a service for managing custom template data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) { func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
var customTemplates = make([]portainer.CustomTemplate, 0) var customTemplates = make([]portainer.CustomTemplate, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -56,7 +56,7 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
var customTemplate portainer.CustomTemplate var customTemplate portainer.CustomTemplate
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate) err := internal.GetObject(service.connection, BucketName, identifier, &customTemplate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -67,18 +67,18 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
// UpdateCustomTemplate updates an custom template. // UpdateCustomTemplate updates an custom template.
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error { func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, customTemplate) return internal.UpdateObject(service.connection, BucketName, identifier, customTemplate)
} }
// DeleteCustomTemplate deletes an custom template. // DeleteCustomTemplate deletes an custom template.
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error { func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// CreateCustomTemplate assign an ID to a new custom template and saves it. // CreateCustomTemplate assign an ID to a new custom template and saves it.
func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error { func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(customTemplate) data, err := internal.MarshalObject(customTemplate)
@ -92,5 +92,5 @@ func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTem
// GetNextIdentifier returns the next identifier for a custom template. // GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }

View File

@ -1,6 +1,7 @@
package bolt package bolt
import ( import (
"io"
"log" "log"
"path" "path"
"time" "time"
@ -17,6 +18,7 @@ import (
"github.com/portainer/portainer/api/bolt/endpointrelation" "github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension" "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/migrator"
"github.com/portainer/portainer/api/bolt/registry" "github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol" "github.com/portainer/portainer/api/bolt/resourcecontrol"
@ -42,7 +44,7 @@ const (
// BoltDB as the storage system. // BoltDB as the storage system.
type Store struct { type Store struct {
path string path string
db *bolt.DB connection *internal.DbConnection
isNew bool isNew bool
fileService portainer.FileService fileService portainer.FileService
CustomTemplateService *customtemplate.Service CustomTemplateService *customtemplate.Service
@ -83,6 +85,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
path: storePath, path: storePath,
fileService: fileService, fileService: fileService,
isNew: true, isNew: true,
connection: &internal.DbConnection{},
} }
databasePath := path.Join(storePath, databaseFileName) databasePath := path.Join(storePath, databaseFileName)
@ -105,15 +108,15 @@ func (store *Store) Open() error {
if err != nil { if err != nil {
return err return err
} }
store.db = db store.connection.DB = db
return store.initServices() return store.initServices()
} }
// Close closes the BoltDB database. // Close closes the BoltDB database.
func (store *Store) Close() error { func (store *Store) Close() error {
if store.db != nil { if store.connection.DB != nil {
return store.db.Close() return store.connection.Close()
} }
return nil return nil
} }
@ -134,8 +137,9 @@ func (store *Store) CheckCurrentEdition() error {
// MigrateData automatically migrate the data based on the DBVersion. // 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. // This process is only triggered on an existing database, not if the database was just created.
func (store *Store) MigrateData() error { // if force is true, then migrate regardless.
if store.isNew { func (store *Store) MigrateData(force bool) error {
if store.isNew && !force {
return store.VersionService.StoreDBVersion(portainer.DBVersion) return store.VersionService.StoreDBVersion(portainer.DBVersion)
} }
@ -148,7 +152,7 @@ func (store *Store) MigrateData() error {
if version < portainer.DBVersion { if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{ migratorParams := &migrator.Parameters{
DB: store.db, DB: store.connection.DB,
DatabaseVersion: version, DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
@ -180,238 +184,11 @@ func (store *Store) MigrateData() error {
return nil return nil
} }
func (store *Store) initServices() error { // BackupTo backs up db to a provided writer.
authorizationsetService, err := role.NewService(store.db) // It does hot backup and doesn't block other database reads and writes
if err != nil { func (store *Store) BackupTo(w io.Writer) error {
return store.connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err return err
} })
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.db)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.db)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.db)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.db)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.db)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.db)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.db)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.db)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.db)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.db)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.db)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.db)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.db)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.db)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.db)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.db)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.db)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.db)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
} }

View File

@ -1,10 +1,8 @@
package dockerhub package dockerhub
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
) )
const ( const (
@ -15,18 +13,18 @@ const (
// Service represents a service for managing Dockerhub data. // Service represents a service for managing Dockerhub data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) DockerHub() (*portainer.DockerHub, error) { func (service *Service) DockerHub() (*portainer.DockerHub, error) {
var dockerhub portainer.DockerHub var dockerhub portainer.DockerHub
err := internal.GetObject(service.db, BucketName, []byte(dockerHubKey), &dockerhub) err := internal.GetObject(service.connection, BucketName, []byte(dockerHubKey), &dockerhub)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,5 +42,5 @@ func (service *Service) DockerHub() (*portainer.DockerHub, error) {
// UpdateDockerHub updates a DockerHub object. // UpdateDockerHub updates a DockerHub object.
func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error { func (service *Service) UpdateDockerHub(dockerhub *portainer.DockerHub) error {
return internal.UpdateObject(service.db, BucketName, []byte(dockerHubKey), dockerhub) return internal.UpdateObject(service.connection, BucketName, []byte(dockerHubKey), dockerhub)
} }

View File

@ -2,7 +2,7 @@ package edgegroup
import ( import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
) )
@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge group data. // Service represents a service for managing Edge group data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) { func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0) var groups = make([]portainer.EdgeGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -56,7 +56,7 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
var group portainer.EdgeGroup var group portainer.EdgeGroup
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group) err := internal.GetObject(service.connection, BucketName, identifier, &group)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -67,18 +67,18 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
// UpdateEdgeGroup updates an Edge group. // UpdateEdgeGroup updates an Edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error { func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, group) return internal.UpdateObject(service.connection, BucketName, identifier, group)
} }
// DeleteEdgeGroup deletes an Edge group. // DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error { func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// CreateEdgeGroup assign an ID to a new Edge group and saves it. // CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error { func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()

View File

@ -2,7 +2,7 @@ package edgejob
import ( import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
) )
@ -13,18 +13,18 @@ const (
// Service represents a service for managing edge jobs data. // Service represents a service for managing edge jobs data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) { func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0) var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -56,7 +56,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
var edgeJob portainer.EdgeJob var edgeJob portainer.EdgeJob
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob) err := internal.GetObject(service.connection, BucketName, identifier, &edgeJob)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -66,7 +66,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
// CreateEdgeJob creates a new Edge job // CreateEdgeJob creates a new Edge job
func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error { func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
if edgeJob.ID == 0 { if edgeJob.ID == 0 {
@ -86,16 +86,16 @@ func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
// UpdateEdgeJob updates an Edge job by ID // UpdateEdgeJob updates an Edge job by ID
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error { func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeJob) return internal.UpdateObject(service.connection, BucketName, identifier, edgeJob)
} }
// DeleteEdgeJob deletes an Edge job // DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error { func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// GetNextIdentifier returns the next identifier for an endpoint. // GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }

View File

@ -2,7 +2,7 @@ package edgestack
import ( import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
) )
@ -13,18 +13,18 @@ const (
// Service represents a service for managing Edge stack data. // Service represents a service for managing Edge stack data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) { func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 0) var stacks = make([]portainer.EdgeStack, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -56,7 +56,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
var stack portainer.EdgeStack var stack portainer.EdgeStack
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack) err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -66,7 +66,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
// CreateEdgeStack assign an ID to a new Edge stack and saves it. // CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error { func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 { if edgeStack.ID == 0 {
@ -86,16 +86,16 @@ func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
// UpdateEdgeStack updates an Edge stack. // UpdateEdgeStack updates an Edge stack.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error { func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack) return internal.UpdateObject(service.connection, BucketName, identifier, edgeStack)
} }
// DeleteEdgeStack deletes an Edge stack. // DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error { func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// GetNextIdentifier returns the next identifier for an endpoint. // GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }

View File

@ -2,7 +2,7 @@ package endpoint
import ( import (
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
) )
@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -33,7 +33,7 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
var endpoint portainer.Endpoint var endpoint portainer.Endpoint
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpoint) err := internal.GetObject(service.connection, BucketName, identifier, &endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,20 +44,20 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
// UpdateEndpoint updates an endpoint. // UpdateEndpoint updates an endpoint.
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error { func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpoint) return internal.UpdateObject(service.connection, BucketName, identifier, endpoint)
} }
// DeleteEndpoint deletes an endpoint. // DeleteEndpoint deletes an endpoint.
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error { func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// Endpoints return an array containing all the endpoints. // Endpoints return an array containing all the endpoints.
func (service *Service) Endpoints() ([]portainer.Endpoint, error) { func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0) var endpoints = make([]portainer.Endpoint, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -78,7 +78,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
// CreateEndpoint assign an ID to a new endpoint and saves it. // CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error { func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for endpoints // We manually manage sequences for endpoints
@ -98,12 +98,12 @@ func (service *Service) CreateEndpoint(endpoint *portainer.Endpoint) error {
// GetNextIdentifier returns the next identifier for an endpoint. // GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }
// Synchronize creates, updates and deletes endpoints inside a single transaction. // Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error { func (service *Service) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
for _, endpoint := range toCreate { for _, endpoint := range toCreate {

View File

@ -1,7 +1,7 @@
package endpointgroup package endpointgroup
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
var endpointGroup portainer.EndpointGroup var endpointGroup portainer.EndpointGroup
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup) err := internal.GetObject(service.connection, BucketName, identifier, &endpointGroup)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,20 +45,20 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
// UpdateEndpointGroup updates an endpoint group. // UpdateEndpointGroup updates an endpoint group.
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error { func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointGroup) return internal.UpdateObject(service.connection, BucketName, identifier, endpointGroup)
} }
// DeleteEndpointGroup deletes an endpoint group. // DeleteEndpointGroup deletes an endpoint group.
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error { func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// EndpointGroups return an array containing all the endpoint groups. // EndpointGroups return an array containing all the endpoint groups.
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) { func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0) var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -79,7 +79,7 @@ func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
// CreateEndpointGroup assign an ID to a new endpoint group and saves it. // CreateEndpointGroup assign an ID to a new endpoint group and saves it.
func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error { func (service *Service) CreateEndpointGroup(endpointGroup *portainer.EndpointGroup) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()

View File

@ -13,18 +13,18 @@ const (
// Service represents a service for managing endpoint relation data. // Service represents a service for managing endpoint relation data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -33,7 +33,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
var endpointRelation portainer.EndpointRelation var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID)) identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation) err := internal.GetObject(service.connection, BucketName, identifier, &endpointRelation)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -43,7 +43,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
// CreateEndpointRelation saves endpointRelation // CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error { func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation) data, err := internal.MarshalObject(endpointRelation)
@ -58,11 +58,11 @@ func (service *Service) CreateEndpointRelation(endpointRelation *portainer.Endpo
// UpdateEndpointRelation updates an Endpoint relation object // UpdateEndpointRelation updates an Endpoint relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error { func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := internal.Itob(int(EndpointID)) identifier := internal.Itob(int(EndpointID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation) return internal.UpdateObject(service.connection, BucketName, identifier, endpointRelation)
} }
// DeleteEndpointRelation deletes an Endpoint relation object // DeleteEndpointRelation deletes an Endpoint relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error { func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := internal.Itob(int(EndpointID)) identifier := internal.Itob(int(EndpointID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -1,7 +1,7 @@
package extension package extension
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
var extension portainer.Extension var extension portainer.Extension
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &extension) err := internal.GetObject(service.connection, BucketName, identifier, &extension)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -46,7 +46,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
func (service *Service) Extensions() ([]portainer.Extension, error) { func (service *Service) Extensions() ([]portainer.Extension, error) {
var extensions = make([]portainer.Extension, 0) var extensions = make([]portainer.Extension, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -67,7 +67,7 @@ func (service *Service) Extensions() ([]portainer.Extension, error) {
// Persist persists a extension inside the database. // Persist persists a extension inside the database.
func (service *Service) Persist(extension *portainer.Extension) error { func (service *Service) Persist(extension *portainer.Extension) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(extension) data, err := internal.MarshalObject(extension)
@ -82,5 +82,5 @@ func (service *Service) Persist(extension *portainer.Extension) error {
// DeleteExtension deletes a Extension. // DeleteExtension deletes a Extension.
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error { func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -7,6 +7,10 @@ import (
"github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/errors"
) )
type DbConnection struct {
*bolt.DB
}
// Itob returns an 8-byte big endian representation of v. // Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices // This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys. // so that they can be used as BoltDB keys.
@ -17,8 +21,8 @@ func Itob(v int) []byte {
} }
// CreateBucket is a generic function used to create a bucket inside a bolt database. // CreateBucket is a generic function used to create a bucket inside a bolt database.
func CreateBucket(db *bolt.DB, bucketName string) error { func CreateBucket(connection *DbConnection, bucketName string) error {
return db.Update(func(tx *bolt.Tx) error { return connection.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName)) _, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil { if err != nil {
return err return err
@ -28,10 +32,10 @@ func CreateBucket(db *bolt.DB, bucketName string) error {
} }
// GetObject is a generic function used to retrieve an unmarshalled object from a bolt database. // GetObject is a generic function used to retrieve an unmarshalled object from a bolt database.
func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error { func GetObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
var data []byte var data []byte
err := db.View(func(tx *bolt.Tx) error { err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName)) bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key) value := bucket.Get(key)
@ -52,8 +56,8 @@ func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) e
} }
// UpdateObject is a generic function used to update an object inside a bolt database. // UpdateObject is a generic function used to update an object inside a bolt database.
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error { func UpdateObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
return db.Update(func(tx *bolt.Tx) error { return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName)) bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object) data, err := MarshalObject(object)
@ -71,18 +75,18 @@ func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}
} }
// DeleteObject is a generic function used to delete an object inside a bolt database. // DeleteObject is a generic function used to delete an object inside a bolt database.
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error { func DeleteObject(connection *DbConnection, bucketName string, key []byte) error {
return db.Update(func(tx *bolt.Tx) error { return connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName)) bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key) return bucket.Delete(key)
}) })
} }
// GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1. // GetNextIdentifier is a generic function that returns the specified bucket identifier incremented by 1.
func GetNextIdentifier(db *bolt.DB, bucketName string) int { func GetNextIdentifier(connection *DbConnection, bucketName string) int {
var identifier int var identifier int
db.Update(func(tx *bolt.Tx) error { connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName)) bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence() id, err := bucket.NextSequence()
if err != nil { if err != nil {

View File

@ -1,7 +1,7 @@
package registry package registry
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
var registry portainer.Registry var registry portainer.Registry
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &registry) err := internal.GetObject(service.connection, BucketName, identifier, &registry)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -46,7 +46,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
func (service *Service) Registries() ([]portainer.Registry, error) { func (service *Service) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0) var registries = make([]portainer.Registry, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -67,7 +67,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
// CreateRegistry creates a new registry. // CreateRegistry creates a new registry.
func (service *Service) CreateRegistry(registry *portainer.Registry) error { func (service *Service) CreateRegistry(registry *portainer.Registry) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -85,11 +85,11 @@ func (service *Service) CreateRegistry(registry *portainer.Registry) error {
// UpdateRegistry updates an registry. // UpdateRegistry updates an registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error { func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, registry) return internal.UpdateObject(service.connection, BucketName, identifier, registry)
} }
// DeleteRegistry deletes an registry. // DeleteRegistry deletes an registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error { func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -1,7 +1,7 @@
package resourcecontrol package resourcecontrol
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
var resourceControl portainer.ResourceControl var resourceControl portainer.ResourceControl
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl) err := internal.GetObject(service.connection, BucketName, identifier, &resourceControl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,7 +48,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl var resourceControl *portainer.ResourceControl
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -82,7 +82,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) { func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0) var rcs = make([]portainer.ResourceControl, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -103,7 +103,7 @@ func (service *Service) ResourceControls() ([]portainer.ResourceControl, error)
// CreateResourceControl creates a new ResourceControl object // CreateResourceControl creates a new ResourceControl object
func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error { func (service *Service) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -121,11 +121,11 @@ func (service *Service) CreateResourceControl(resourceControl *portainer.Resourc
// UpdateResourceControl saves a ResourceControl object. // UpdateResourceControl saves a ResourceControl object.
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error { func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, resourceControl) return internal.UpdateObject(service.connection, BucketName, identifier, resourceControl)
} }
// DeleteResourceControl deletes a ResourceControl object by ID // DeleteResourceControl deletes a ResourceControl object by ID
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error { func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -1,7 +1,7 @@
package role package role
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
var set portainer.Role var set portainer.Role
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &set) err := internal.GetObject(service.connection, BucketName, identifier, &set)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -46,7 +46,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
func (service *Service) Roles() ([]portainer.Role, error) { func (service *Service) Roles() ([]portainer.Role, error) {
var sets = make([]portainer.Role, 0) var sets = make([]portainer.Role, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -67,7 +67,7 @@ func (service *Service) Roles() ([]portainer.Role, error) {
// CreateRole creates a new Role. // CreateRole creates a new Role.
func (service *Service) CreateRole(role *portainer.Role) error { func (service *Service) CreateRole(role *portainer.Role) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -85,5 +85,5 @@ func (service *Service) CreateRole(role *portainer.Role) error {
// UpdateRole updates a role. // UpdateRole updates a role.
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error { func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, role) return internal.UpdateObject(service.connection, BucketName, identifier, role)
} }

View File

@ -1,7 +1,7 @@
package schedule package schedule
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing schedule data. // Service represents a service for managing schedule data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
var schedule portainer.Schedule var schedule portainer.Schedule
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &schedule) err := internal.GetObject(service.connection, BucketName, identifier, &schedule)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -45,20 +45,20 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
// UpdateSchedule updates a schedule. // UpdateSchedule updates a schedule.
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error { func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, schedule) return internal.UpdateObject(service.connection, BucketName, identifier, schedule)
} }
// DeleteSchedule deletes a schedule. // DeleteSchedule deletes a schedule.
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error { func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// Schedules return a array containing all the schedules. // Schedules return a array containing all the schedules.
func (service *Service) Schedules() ([]portainer.Schedule, error) { func (service *Service) Schedules() ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0) var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -82,7 +82,7 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) { func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
var schedules = make([]portainer.Schedule, 0) var schedules = make([]portainer.Schedule, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -105,7 +105,7 @@ func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portain
// CreateSchedule assign an ID to a new schedule and saves it. // CreateSchedule assign an ID to a new schedule and saves it.
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error { func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for schedules // We manually manage sequences for schedules
@ -125,5 +125,5 @@ func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
// GetNextIdentifier returns the next identifier for a schedule. // GetNextIdentifier returns the next identifier for a schedule.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }

263
api/bolt/services.go Normal file
View File

@ -0,0 +1,263 @@
package bolt
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgejob"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
"github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/team"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
)
func (store *Store) initServices() error {
authorizationsetService, err := role.NewService(store.connection)
if err != nil {
return err
}
store.RoleService = authorizationsetService
customTemplateService, err := customtemplate.NewService(store.connection)
if err != nil {
return err
}
store.CustomTemplateService = customTemplateService
dockerhubService, err := dockerhub.NewService(store.connection)
if err != nil {
return err
}
store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
edgeJobService, err := edgejob.NewService(store.connection)
if err != nil {
return err
}
store.EdgeJobService = edgeJobService
endpointgroupService, err := endpointgroup.NewService(store.connection)
if err != nil {
return err
}
store.EndpointGroupService = endpointgroupService
endpointService, err := endpoint.NewService(store.connection)
if err != nil {
return err
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.connection)
if err != nil {
return err
}
store.ExtensionService = extensionService
registryService, err := registry.NewService(store.connection)
if err != nil {
return err
}
store.RegistryService = registryService
resourcecontrolService, err := resourcecontrol.NewService(store.connection)
if err != nil {
return err
}
store.ResourceControlService = resourcecontrolService
settingsService, err := settings.NewService(store.connection)
if err != nil {
return err
}
store.SettingsService = settingsService
stackService, err := stack.NewService(store.connection)
if err != nil {
return err
}
store.StackService = stackService
tagService, err := tag.NewService(store.connection)
if err != nil {
return err
}
store.TagService = tagService
teammembershipService, err := teammembership.NewService(store.connection)
if err != nil {
return err
}
store.TeamMembershipService = teammembershipService
teamService, err := team.NewService(store.connection)
if err != nil {
return err
}
store.TeamService = teamService
tunnelServerService, err := tunnelserver.NewService(store.connection)
if err != nil {
return err
}
store.TunnelServerService = tunnelServerService
userService, err := user.NewService(store.connection)
if err != nil {
return err
}
store.UserService = userService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
}
store.VersionService = versionService
webhookService, err := webhook.NewService(store.connection)
if err != nil {
return err
}
store.WebhookService = webhookService
scheduleService, err := schedule.NewService(store.connection)
if err != nil {
return err
}
store.ScheduleService = scheduleService
return nil
}
// CustomTemplate gives access to the CustomTemplate data management layer
func (store *Store) CustomTemplate() portainer.CustomTemplateService {
return store.CustomTemplateService
}
// DockerHub gives access to the DockerHub data management layer
func (store *Store) DockerHub() portainer.DockerHubService {
return store.DockerHubService
}
// EdgeGroup gives access to the EdgeGroup data management layer
func (store *Store) EdgeGroup() portainer.EdgeGroupService {
return store.EdgeGroupService
}
// EdgeJob gives access to the EdgeJob data management layer
func (store *Store) EdgeJob() portainer.EdgeJobService {
return store.EdgeJobService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() portainer.EdgeStackService {
return store.EdgeStackService
}
// Endpoint gives access to the Endpoint data management layer
func (store *Store) Endpoint() portainer.EndpointService {
return store.EndpointService
}
// EndpointGroup gives access to the EndpointGroup data management layer
func (store *Store) EndpointGroup() portainer.EndpointGroupService {
return store.EndpointGroupService
}
// EndpointRelation gives access to the EndpointRelation data management layer
func (store *Store) EndpointRelation() portainer.EndpointRelationService {
return store.EndpointRelationService
}
// Registry gives access to the Registry data management layer
func (store *Store) Registry() portainer.RegistryService {
return store.RegistryService
}
// ResourceControl gives access to the ResourceControl data management layer
func (store *Store) ResourceControl() portainer.ResourceControlService {
return store.ResourceControlService
}
// Role gives access to the Role data management layer
func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService
}
// Stack gives access to the Stack data management layer
func (store *Store) Stack() portainer.StackService {
return store.StackService
}
// Tag gives access to the Tag data management layer
func (store *Store) Tag() portainer.TagService {
return store.TagService
}
// TeamMembership gives access to the TeamMembership data management layer
func (store *Store) TeamMembership() portainer.TeamMembershipService {
return store.TeamMembershipService
}
// Team gives access to the Team data management layer
func (store *Store) Team() portainer.TeamService {
return store.TeamService
}
// TunnelServer gives access to the TunnelServer data management layer
func (store *Store) TunnelServer() portainer.TunnelServerService {
return store.TunnelServerService
}
// User gives access to the User data management layer
func (store *Store) User() portainer.UserService {
return store.UserService
}
// Version gives access to the Version data management layer
func (store *Store) Version() portainer.VersionService {
return store.VersionService
}
// Webhook gives access to the Webhook data management layer
func (store *Store) Webhook() portainer.WebhookService {
return store.WebhookService
}

View File

@ -1,10 +1,8 @@
package settings package settings
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
) )
const ( const (
@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Settings() (*portainer.Settings, error) { func (service *Service) Settings() (*portainer.Settings, error) {
var settings portainer.Settings var settings portainer.Settings
err := internal.GetObject(service.db, BucketName, []byte(settingsKey), &settings) err := internal.GetObject(service.connection, BucketName, []byte(settingsKey), &settings)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,5 +42,5 @@ func (service *Service) Settings() (*portainer.Settings, error) {
// UpdateSettings persists a Settings object. // UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error { func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.db, BucketName, []byte(settingsKey), settings) return internal.UpdateObject(service.connection, BucketName, []byte(settingsKey), settings)
} }

View File

@ -1,7 +1,7 @@
package stack package stack
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
@ -15,18 +15,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -35,7 +35,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
var stack portainer.Stack var stack portainer.Stack
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack) err := internal.GetObject(service.connection, BucketName, identifier, &stack)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,7 +47,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
func (service *Service) StackByName(name string) (*portainer.Stack, error) { func (service *Service) StackByName(name string) (*portainer.Stack, error) {
var stack *portainer.Stack var stack *portainer.Stack
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -78,7 +78,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
func (service *Service) Stacks() ([]portainer.Stack, error) { func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0) var stacks = make([]portainer.Stack, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -99,12 +99,12 @@ func (service *Service) Stacks() ([]portainer.Stack, error) {
// GetNextIdentifier returns the next identifier for a stack. // GetNextIdentifier returns the next identifier for a stack.
func (service *Service) GetNextIdentifier() int { func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName) return internal.GetNextIdentifier(service.connection, BucketName)
} }
// CreateStack creates a new stack. // CreateStack creates a new stack.
func (service *Service) CreateStack(stack *portainer.Stack) error { func (service *Service) CreateStack(stack *portainer.Stack) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
// We manually manage sequences for stacks // We manually manage sequences for stacks
@ -125,11 +125,11 @@ func (service *Service) CreateStack(stack *portainer.Stack) error {
// UpdateStack updates a stack. // UpdateStack updates a stack.
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error { func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, stack) return internal.UpdateObject(service.connection, BucketName, identifier, stack)
} }
// DeleteStack deletes a stack. // DeleteStack deletes a stack.
func (service *Service) DeleteStack(ID portainer.StackID) error { func (service *Service) DeleteStack(ID portainer.StackID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -1,7 +1,7 @@
package tag package tag
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -33,7 +33,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Tags() ([]portainer.Tag, error) { func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0) var tags = make([]portainer.Tag, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -57,7 +57,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag var tag portainer.Tag
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &tag) err := internal.GetObject(service.connection, BucketName, identifier, &tag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -67,7 +67,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
// CreateTag creates a new tag. // CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error { func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -85,11 +85,11 @@ func (service *Service) CreateTag(tag *portainer.Tag) error {
// UpdateTag updates a tag. // UpdateTag updates a tag.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error { func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, tag) return internal.UpdateObject(service.connection, BucketName, identifier, tag)
} }
// DeleteTag deletes a tag. // DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error { func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -17,18 +17,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -37,7 +37,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
var team portainer.Team var team portainer.Team
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &team) err := internal.GetObject(service.connection, BucketName, identifier, &team)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -49,7 +49,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
func (service *Service) TeamByName(name string) (*portainer.Team, error) { func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var team *portainer.Team var team *portainer.Team
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -80,7 +80,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func (service *Service) Teams() ([]portainer.Team, error) { func (service *Service) Teams() ([]portainer.Team, error) {
var teams = make([]portainer.Team, 0) var teams = make([]portainer.Team, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -102,12 +102,12 @@ func (service *Service) Teams() ([]portainer.Team, error) {
// UpdateTeam saves a Team. // UpdateTeam saves a Team.
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error { func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, team) return internal.UpdateObject(service.connection, BucketName, identifier, team)
} }
// CreateTeam creates a new Team. // CreateTeam creates a new Team.
func (service *Service) CreateTeam(team *portainer.Team) error { func (service *Service) CreateTeam(team *portainer.Team) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -125,5 +125,5 @@ func (service *Service) CreateTeam(team *portainer.Team) error {
// DeleteTeam deletes a Team. // DeleteTeam deletes a Team.
func (service *Service) DeleteTeam(ID portainer.TeamID) error { func (service *Service) DeleteTeam(ID portainer.TeamID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -1,7 +1,7 @@
package teammembership package teammembership
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -14,18 +14,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
var membership portainer.TeamMembership var membership portainer.TeamMembership
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &membership) err := internal.GetObject(service.connection, BucketName, identifier, &membership)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -46,7 +46,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) { func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0) var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -69,7 +69,7 @@ func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) { func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0) var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -95,7 +95,7 @@ func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]port
func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) { func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
var memberships = make([]portainer.TeamMembership, 0) var memberships = make([]portainer.TeamMembership, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -120,12 +120,12 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
// UpdateTeamMembership saves a TeamMembership object. // UpdateTeamMembership saves a TeamMembership object.
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error { func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, membership) return internal.UpdateObject(service.connection, BucketName, identifier, membership)
} }
// CreateTeamMembership creates a new TeamMembership object. // CreateTeamMembership creates a new TeamMembership object.
func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error { func (service *Service) CreateTeamMembership(membership *portainer.TeamMembership) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -143,12 +143,12 @@ func (service *Service) CreateTeamMembership(membership *portainer.TeamMembershi
// DeleteTeamMembership deletes a TeamMembership object. // DeleteTeamMembership deletes a TeamMembership object.
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error { func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID. // DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error { func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -173,7 +173,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID. // DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error { func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()

View File

@ -1,10 +1,8 @@
package tunnelserver package tunnelserver
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
) )
const ( const (
@ -15,18 +13,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Info() (*portainer.TunnelServerInfo, error) { func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
var info portainer.TunnelServerInfo var info portainer.TunnelServerInfo
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info) err := internal.GetObject(service.connection, BucketName, []byte(infoKey), &info)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -44,5 +42,5 @@ func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
// UpdateInfo persists a TunnelServerInfo object. // UpdateInfo persists a TunnelServerInfo object.
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error { func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings) return internal.UpdateObject(service.connection, BucketName, []byte(infoKey), settings)
} }

View File

@ -17,18 +17,18 @@ const (
// Service represents a service for managing endpoint data. // Service represents a service for managing endpoint data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -37,7 +37,7 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
var user portainer.User var user portainer.User
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &user) err := internal.GetObject(service.connection, BucketName, identifier, &user)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -51,7 +51,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
username = strings.ToLower(username) username = strings.ToLower(username)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -81,7 +81,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
func (service *Service) Users() ([]portainer.User, error) { func (service *Service) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0) var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -103,7 +103,7 @@ func (service *Service) Users() ([]portainer.User, error) {
// UsersByRole return an array containing all the users with the specified role. // UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) { func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0) var users = make([]portainer.User, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -128,12 +128,12 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error { func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
user.Username = strings.ToLower(user.Username) user.Username = strings.ToLower(user.Username)
return internal.UpdateObject(service.db, BucketName, identifier, user) return internal.UpdateObject(service.connection, BucketName, identifier, user)
} }
// CreateUser creates a new user. // CreateUser creates a new user.
func (service *Service) CreateUser(user *portainer.User) error { func (service *Service) CreateUser(user *portainer.User) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()
@ -152,5 +152,5 @@ func (service *Service) CreateUser(user *portainer.User) error {
// DeleteUser deletes a user. // DeleteUser deletes a user.
func (service *Service) DeleteUser(ID portainer.UserID) error { func (service *Service) DeleteUser(ID portainer.UserID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }

View File

@ -19,18 +19,18 @@ const (
// Service represents a service to manage stored versions. // Service represents a service to manage stored versions.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -38,7 +38,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) DBVersion() (int, error) { func (service *Service) DBVersion() (int, error) {
var data []byte var data []byte
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(versionKey)) value := bucket.Get([]byte(versionKey))
@ -75,7 +75,7 @@ func (service *Service) Edition() (portainer.SoftwareEdition, error) {
// StoreDBVersion store the database version. // StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error { func (service *Service) StoreDBVersion(version int) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data := []byte(strconv.Itoa(version)) data := []byte(strconv.Itoa(version))
@ -87,7 +87,7 @@ func (service *Service) StoreDBVersion(version int) error {
func (service *Service) InstanceID() (string, error) { func (service *Service) InstanceID() (string, error) {
var data []byte var data []byte
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(instanceKey)) value := bucket.Get([]byte(instanceKey))
@ -109,7 +109,7 @@ func (service *Service) InstanceID() (string, error) {
// StoreInstanceID store the instance ID. // StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error { func (service *Service) StoreInstanceID(ID string) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data := []byte(ID) data := []byte(ID)
@ -120,7 +120,7 @@ func (service *Service) StoreInstanceID(ID string) error {
func (service *Service) getKey(key string) ([]byte, error) { func (service *Service) getKey(key string) ([]byte, error) {
var data []byte var data []byte
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
value := bucket.Get([]byte(key)) value := bucket.Get([]byte(key))
@ -142,7 +142,7 @@ func (service *Service) getKey(key string) ([]byte, error) {
} }
func (service *Service) setKey(key string, value string) error { func (service *Service) setKey(key string, value string) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
data := []byte(value) data := []byte(value)

View File

@ -1,7 +1,7 @@
package webhook package webhook
import ( import (
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal" "github.com/portainer/portainer/api/bolt/internal"
@ -15,18 +15,18 @@ const (
// Service represents a service for managing webhook data. // Service represents a service for managing webhook data.
type Service struct { type Service struct {
db *bolt.DB connection *internal.DbConnection
} }
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(db *bolt.DB) (*Service, error) { func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(db, BucketName) err := internal.CreateBucket(connection, BucketName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Service{ return &Service{
db: db, connection: connection,
}, nil }, nil
} }
@ -34,7 +34,7 @@ func NewService(db *bolt.DB) (*Service, error) {
func (service *Service) Webhooks() ([]portainer.Webhook, error) { func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0) var webhooks = make([]portainer.Webhook, 0)
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -58,7 +58,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
var webhook portainer.Webhook var webhook portainer.Webhook
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &webhook) err := internal.GetObject(service.connection, BucketName, identifier, &webhook)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,7 +70,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) { func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -101,7 +101,7 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) { func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) {
var webhook *portainer.Webhook var webhook *portainer.Webhook
err := service.db.View(func(tx *bolt.Tx) error { err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor() cursor := bucket.Cursor()
@ -131,12 +131,12 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
// DeleteWebhook deletes a webhook. // DeleteWebhook deletes a webhook.
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error { func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier) return internal.DeleteObject(service.connection, BucketName, identifier)
} }
// CreateWebhook assign an ID to a new webhook and saves it. // CreateWebhook assign an ID to a new webhook and saves it.
func (service *Service) CreateWebhook(webhook *portainer.Webhook) error { func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName)) bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence() id, _ := bucket.NextSequence()

View File

@ -1,6 +1,7 @@
package chisel package chisel
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
@ -9,7 +10,7 @@ import (
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server" chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map" cmap "github.com/orcaman/concurrent-map"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/errors"
) )
@ -29,13 +30,15 @@ type Service struct {
dataStore portainer.DataStore dataStore portainer.DataStore
snapshotService portainer.SnapshotService snapshotService portainer.SnapshotService
chiselServer *chserver.Server chiselServer *chserver.Server
shutdownCtx context.Context
} }
// NewService returns a pointer to a new instance of Service // NewService returns a pointer to a new instance of Service
func NewService(dataStore portainer.DataStore) *Service { func NewService(dataStore portainer.DataStore, shutdownCtx context.Context) *Service {
return &Service{ return &Service{
tunnelDetailsMap: cmap.New(), tunnelDetailsMap: cmap.New(),
dataStore: dataStore, dataStore: dataStore,
shutdownCtx: shutdownCtx,
} }
} }
@ -83,6 +86,11 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
return nil return nil
} }
// StopTunnelServer stops tunnel http server
func (service *Service) StopTunnelServer() error {
return service.chiselServer.Close()
}
func (service *Service) retrievePrivateKeySeed() (string, error) { func (service *Service) retrievePrivateKeySeed() (string, error) {
var serverInfo *portainer.TunnelServerInfo var serverInfo *portainer.TunnelServerInfo
@ -108,13 +116,16 @@ func (service *Service) retrievePrivateKeySeed() (string, error) {
func (service *Service) startTunnelVerificationLoop() { func (service *Service) startTunnelVerificationLoop() {
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds()) log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
ticker := time.NewTicker(tunnelCleanupInterval) ticker := time.NewTicker(tunnelCleanupInterval)
stopSignal := make(chan struct{})
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
service.checkTunnels() service.checkTunnels()
case <-stopSignal: case <-service.shutdownCtx.Done():
log.Println("[DEBUG] Shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Printf("Stopped tunnel service: %s", err)
}
ticker.Stop() ticker.Stop()
return return
} }

View File

@ -1,10 +1,10 @@
package main package main
import ( import (
"context"
"log" "log"
"os" "os"
"strings" "strings"
"time"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt" "github.com/portainer/portainer/api/bolt"
@ -20,6 +20,7 @@ import (
"github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes" "github.com/portainer/portainer/api/kubernetes"
@ -67,7 +68,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
log.Fatalf("failed initializing data store: %v", err) log.Fatalf("failed initializing data store: %v", err)
} }
err = store.MigrateData() err = store.MigrateData(false)
if err != nil { if err != nil {
log.Fatalf("failed migration: %v", err) log.Fatalf("failed migration: %v", err)
} }
@ -136,11 +137,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID) return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
} }
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) { func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory) dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory) kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter) snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -148,21 +149,6 @@ func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore,
return snapshotService, nil return snapshotService, nil
} }
func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().EdgeJobs()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
}
}
return nil
}
func initStatus(flags *portainer.CLIFlags) *portainer.Status { func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{ return &portainer.Status{
Version: portainer.APIVersion, Version: portainer.APIVersion,
@ -353,28 +339,12 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService) return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
} }
func terminateIfNoAdminCreated(dataStore portainer.DataStore) { func buildServer(flags *portainer.CLIFlags) portainer.Server {
timer1 := time.NewTimer(5 * time.Minute) shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
<-timer1.C
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatalf("failed getting admin user: %v", err)
}
if len(users) == 0 {
log.Fatal("No administrator account was created after 5 min. Shutting down the Portainer instance for security reasons.")
return
}
}
func main() {
flags := initCLI()
fileService := initFileService(*flags.Data) fileService := initFileService(*flags.Data)
dataStore := initDataStore(*flags.Data, fileService) dataStore := initDataStore(*flags.Data, fileService)
defer dataStore.Close()
if err := dataStore.CheckCurrentEdition(); err != nil { if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err) log.Fatal(err)
@ -400,7 +370,7 @@ func main() {
log.Fatalf("failed initializing key pai: %v", err) log.Fatalf("failed initializing key pai: %v", err)
} }
reverseTunnelService := chisel.NewService(dataStore) reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
instanceID, err := dataStore.Version().InstanceID() instanceID, err := dataStore.Version().InstanceID()
if err != nil { if err != nil {
@ -410,7 +380,7 @@ func main() {
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID) kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory) snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil { if err != nil {
log.Fatalf("failed initializing snapshot service: %v", err) log.Fatalf("failed initializing snapshot service: %v", err)
} }
@ -434,7 +404,7 @@ func main() {
} }
} }
err = loadEdgeJobsFromDatabase(dataStore, reverseTunnelService) err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil { if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err) log.Fatalf("failed loading edge jobs from database: %v", err)
} }
@ -482,14 +452,12 @@ func main() {
} }
} }
go terminateIfNoAdminCreated(dataStore)
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService) err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil { if err != nil {
log.Fatalf("failed starting tunnel server: %v", err) log.Fatalf("failed starting license service: %s", err)
} }
var server portainer.Server = &http.Server{ return &http.Server{
ReverseTunnelService: reverseTunnelService, ReverseTunnelService: reverseTunnelService,
Status: applicationStatus, Status: applicationStatus,
BindAddress: *flags.Addr, BindAddress: *flags.Addr,
@ -513,11 +481,18 @@ func main() {
SSLKey: *flags.SSLKey, SSLKey: *flags.SSLKey,
DockerClientFactory: dockerClientFactory, DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory, KubernetesClientFactory: kubernetesClientFactory,
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
}
} }
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) func main() {
err = server.Start() flags := initCLI()
if err != nil {
log.Fatalf("failed starting server: %v", err) for {
server := buildServer(flags)
log.Printf("Starting Portainer %s on %s\n", portainer.APIVersion, *flags.Addr)
err := server.Start()
log.Printf("Http server exited: %s\n", err)
} }
} }

70
api/crypto/aes.go Normal file
View File

@ -0,0 +1,70 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"io"
"golang.org/x/crypto/scrypt"
)
// NOTE: has to go with what is considered to be a simplistic in that it omits any
// authentication of the encrypted data.
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
var emptySalt []byte = make([]byte, 0, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return err
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
writer := &cipher.StreamWriter{S: stream, W: output}
// Copy the input to the output, encrypting as we go.
if _, err := io.Copy(writer, input); err != nil {
return err
}
return nil
}
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
// passphrase is used to generate an encryption key.
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
// making a 32 bytes key that would correspond to AES-256
// don't necessarily need a salt, so just kept in empty
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// If the key is unique for each ciphertext, then it's ok to use a zero
// IV.
var iv [aes.BlockSize]byte
stream := cipher.NewOFB(block, iv[:])
reader := &cipher.StreamReader{S: stream, R: input}
return reader, nil
}

131
api/crypto/aes_test.go Normal file
View File

@ -0,0 +1,131 @@
package crypto
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("passphrase"))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(""))
assert.Nil(t, err, "Failed to decrypt file")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
tmpdir, _ := os.MkdirTemp("", "encrypt")
defer os.RemoveAll(tmpdir)
var (
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

View File

@ -9,7 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"io" "io"
"os" "os"
@ -505,3 +505,8 @@ func (service *Service) GetTemporaryPath() (string, error) {
return path.Join(service.fileStorePath, TempPath, uid.String()), nil return path.Join(service.fileStorePath, TempPath, uid.String()), nil
} }
// GetDataStorePath returns path to data folder
func (service *Service) GetDatastorePath() string {
return service.dataStorePath
}

View File

@ -25,6 +25,7 @@ require (
github.com/mattn/go-shellwords v1.0.6 // indirect github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/libcompose v0.5.3 github.com/portainer/libcompose v0.5.3
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33

View File

@ -223,6 +223,8 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@ -0,0 +1,53 @@
package backup
import (
"fmt"
"net/http"
"os"
"path/filepath"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
operations "github.com/portainer/portainer/api/backup"
)
type (
backupPayload struct {
Password string
}
)
func (p *backupPayload) Validate(r *http.Request) error {
return nil
}
// @id Backup
// @summary Creates an archive with a system data snapshot that could be used to restore the system.
// @description Creates an archive with a system data snapshot that could be used to restore the system.
// @description **Access policy**: admin
// @tags backup
// @security jwt
// @produce octet-stream
// @param Password body string false "Password to encrypt the backup with"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /backup [post]
func (h *Handler) backup(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload backupPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
archivePath, err := operations.CreateBackupArchive(payload.Password, h.gate, h.dataStore, h.filestorePath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to create backup", Err: err}
}
defer os.RemoveAll(filepath.Dir(archivePath))
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("portainer-backup_%s", filepath.Base(archivePath))))
http.ServeFile(w, r, archivePath)
return nil
}

View File

@ -0,0 +1,121 @@
package backup
import (
"bytes"
"context"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func listFiles(dir string) []string {
items := make([]string, 0)
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
})
return items
}
func contains(t *testing.T, list []string, path string) {
assert.Contains(t, list, path)
copyContent, _ := ioutil.ReadFile(path)
assert.Equal(t, "content\n", string(copyContent))
}
func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"password":""}`))
w := httptest.NewRecorder()
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
body, _ := io.ReadAll(response.Body)
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
archivePath := filepath.Join(tmpdir, "archive.tar.gz")
err := ioutil.WriteFile(archivePath, body, 0600)
if err != nil {
t.Fatal("Failed to save downloaded .tar.gz archive: ", err)
}
cmd := exec.Command("tar", "-xzf", archivePath, "-C", tmpdir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
createdFiles := listFiles(tmpdir)
contains(t, createdFiles, path.Join(tmpdir, "portainer.key"))
contains(t, createdFiles, path.Join(tmpdir, "portainer.pub"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file1"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file2"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_file"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_folder", "file1"))
}
func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"password":"secret"}`))
w := httptest.NewRecorder()
gate := offlinegate.NewOfflineGate()
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
body, _ := io.ReadAll(response.Body)
tmpdir, _ := os.MkdirTemp("", "backup")
defer os.RemoveAll(tmpdir)
dr, err := crypto.AesDecrypt(bytes.NewReader(body), []byte("secret"))
if err != nil {
t.Fatal("Failed to decrypt archive")
}
archivePath := filepath.Join(tmpdir, "archive.tag.gz")
archive, _ := os.Create(archivePath)
defer archive.Close()
io.Copy(archive, dr)
cmd := exec.Command("tar", "-xzf", archivePath, "-C", tmpdir)
err = cmd.Run()
if err != nil {
t.Fatal("Failed to extract archive: ", err)
}
createdFiles := listFiles(tmpdir)
contains(t, createdFiles, path.Join(tmpdir, "portainer.key"))
contains(t, createdFiles, path.Join(tmpdir, "portainer.pub"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file1"))
contains(t, createdFiles, path.Join(tmpdir, "tls", "file2"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_file"))
assert.NotContains(t, createdFiles, path.Join(tmpdir, "extra_folder", "file1"))
}

View File

@ -0,0 +1,65 @@
package backup
import (
"context"
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/http/security"
)
// Handler is an http handler responsible for backup and restore portainer state
type Handler struct {
*mux.Router
bouncer *security.RequestBouncer
dataStore portainer.DataStore
gate *offlinegate.OfflineGate
filestorePath string
shutdownTrigger context.CancelFunc
adminMonitor *adminmonitor.Monitor
}
// NewHandler creates an new instance of backup handler
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
h := &Handler{
Router: mux.NewRouter(),
bouncer: bouncer,
dataStore: dataStore,
gate: gate,
filestorePath: filestorePath,
shutdownTrigger: shutdownTrigger,
adminMonitor: adminMonitor,
}
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
return h
}
func adminAccess(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user info from request context", err)
}
if !securityContext.IsAdmin {
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
}
next.ServeHTTP(w, r)
})
}
func systemWasInitialized(dataStore portainer.DataStore) (bool, error) {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
}

View File

@ -0,0 +1,69 @@
package backup
import (
"bytes"
"io"
"net/http"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
operations "github.com/portainer/portainer/api/backup"
)
type restorePayload struct {
FileContent []byte
FileName string
Password string
}
// @id Restore
// @summary Triggers a system restore using provided backup file
// @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"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /restore [post]
func (h *Handler) restore(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
initialized, err := h.adminMonitor.WasInitialized()
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to check system initialization", Err: err}
}
if initialized {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot restore already initialized instance", Err: errors.New("system already initialized")}
}
h.adminMonitor.Stop()
defer h.adminMonitor.Start()
var payload restorePayload
err = decodeForm(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
var archiveReader io.Reader = bytes.NewReader(payload.FileContent)
err = operations.RestoreArchive(archiveReader, payload.Password, h.filestorePath, h.gate, h.dataStore, h.shutdownTrigger)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to restore the backup", Err: err}
}
return nil
}
func decodeForm(r *http.Request, p *restorePayload) error {
content, name, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return err
}
p.FileContent = content
p.FileName = name
password, _ := request.RetrieveMultiPartFormValue(r, "password", true)
p.Password = password
return nil
}

View File

@ -0,0 +1,123 @@
package backup
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/http/offlinegate"
i "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
tests := []struct {
name string
backupPassword string
restorePassword string
fails bool
}{
{
name: "empty password to both encrypt and decrypt",
backupPassword: "",
restorePassword: "",
fails: false,
},
{
name: "same password to encrypt and decrypt",
backupPassword: "secret",
restorePassword: "secret",
fails: false,
},
{
name: "different passwords to encrypt and decrypt",
backupPassword: "secret",
restorePassword: "terces",
fails: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, test.backupPassword)
//restore
w := httptest.NewRecorder()
r, err := prepareMultipartRequest(test.restorePassword, archive)
assert.Nil(t, err, "Shouldn't fail to write multipart form")
restoreErr := h.restore(w, r)
assert.Equal(t, test.fails, restoreErr != nil, "Didn't meet expectation of failing restore handler")
})
}
}
func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
admin := portainer.User{
Role: portainer.AdministratorRole,
}
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
//backup
archive := backup(t, h, "password")
//restore
w := httptest.NewRecorder()
r, err := prepareMultipartRequest("password", archive)
assert.Nil(t, err, "Shouldn't fail to write multipart form")
restoreErr := h.restore(w, r)
assert.NotNil(t, restoreErr, "Should fail, because system it already initialized")
assert.Equal(t, "Cannot restore already initialized instance", restoreErr.Message, "Should fail with certain error")
}
func backup(t *testing.T, h *Handler, password string) []byte {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(fmt.Sprintf(`{"password":"%s"}`, password)))
w := httptest.NewRecorder()
backupErr := h.backup(w, r)
assert.Nil(t, backupErr, "Backup should not fail")
response := w.Result()
archive, _ := io.ReadAll(response.Body)
return archive
}
func prepareMultipartRequest(password string, file []byte) (*http.Request, error) {
var body bytes.Buffer
w := multipart.NewWriter(&body)
err := w.WriteField("password", password)
if err != nil {
return nil, err
}
fw, err := w.CreateFormFile("file", "filename")
if err != nil {
return nil, err
}
io.Copy(fw, bytes.NewReader(file))
r := httptest.NewRequest(http.MethodPost, "http://localhost/", &body)
r.Header.Set("Content-Type", w.FormDataContentType())
w.Close()
return r, nil
}

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -0,0 +1 @@
content

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/portainer/portainer/api/http/handler/auth" "github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates" "github.com/portainer/portainer/api/http/handler/customtemplates"
"github.com/portainer/portainer/api/http/handler/dockerhub" "github.com/portainer/portainer/api/http/handler/dockerhub"
"github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgegroups"
@ -36,6 +37,7 @@ import (
// Handler is a collection of all the service handlers. // Handler is a collection of all the service handlers.
type Handler struct { type Handler struct {
AuthHandler *auth.Handler AuthHandler *auth.Handler
BackupHandler *backup.Handler
CustomTemplatesHandler *customtemplates.Handler CustomTemplatesHandler *customtemplates.Handler
DockerHubHandler *dockerhub.Handler DockerHubHandler *dockerhub.Handler
EdgeGroupsHandler *edgegroups.Handler EdgeGroupsHandler *edgegroups.Handler
@ -140,6 +142,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch { switch {
case strings.HasPrefix(r.URL.Path, "/api/auth"): case strings.HasPrefix(r.URL.Path, "/api/auth"):
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/backup"):
http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/restore"):
http.StripPrefix("/api", h.BackupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/custom_templates"): case strings.HasPrefix(r.URL.Path, "/api/custom_templates"):

View File

@ -0,0 +1,71 @@
package offlinegate
import (
"log"
"net/http"
"sync"
"time"
httperror "github.com/portainer/libhttp/error"
)
// OfflineGate is a entity that works similar to a mutex with a signaling
// Only the caller that have Locked an gate can unlock it, otherw will be blocked with a call to Lock.
// Gate provides a passthrough http middleware that will wait for a locked gate to be unlocked.
// For a safety reasons, middleware will timeout
type OfflineGate struct {
lock *sync.Mutex
signalingCh chan interface{}
}
// NewOfflineGate creates a new gate
func NewOfflineGate() *OfflineGate {
return &OfflineGate{
lock: &sync.Mutex{},
}
}
// Lock locks readonly gate and returns a function to unlock
func (o *OfflineGate) Lock() func() {
o.lock.Lock()
o.signalingCh = make(chan interface{})
return o.unlock
}
func (o *OfflineGate) unlock() {
if o.signalingCh == nil {
return
}
close(o.signalingCh)
o.signalingCh = nil
o.lock.Unlock()
}
// Watch returns a signaling channel.
// Unless channel is nil, client needs to watch for a signal on a channel to know when gate is unlocked.
// Signal channel is disposable: onced signaled, has to be disposed and acquired again.
func (o *OfflineGate) Watch() chan interface{} {
return o.signalingCh
}
// WaitingMiddleware returns an http handler that waits for the gate to be unlocked before continuing
func (o *OfflineGate) WaitingMiddleware(timeout time.Duration, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
signalingCh := o.Watch()
if signalingCh != nil {
if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" {
select {
case <-signalingCh:
case <-time.After(timeout):
log.Println("error: Timeout waiting for the offline gate to signal")
httperror.WriteError(w, http.StatusRequestTimeout, "Timeout waiting for the offline gate to signal", http.ErrHandlerTimeout)
}
}
}
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,217 @@
package offlinegate
import (
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_canLockAndUnlock(t *testing.T) {
o := NewOfflineGate()
unlock := o.Lock()
unlock()
}
func Test_hasToBeUnlockedToLockAgain(t *testing.T) {
// scenario:
// 1. first routine starts and locks the gate
// 2. first routine starts a second and wait for the second to start
// 3. second start but waits for the gate to be released
// 4. first continues and unlocks the gate, when done
// 5. second be able to continue
// 6. second lock the gate, does the job and unlocks it
o := NewOfflineGate()
wg := sync.WaitGroup{}
wg.Add(2)
result := make([]string, 0, 2)
go func() {
unlock := o.Lock()
defer unlock()
waitForSecondToStart := sync.WaitGroup{}
waitForSecondToStart.Add(1)
go func() {
waitForSecondToStart.Done()
unlock := o.Lock()
defer unlock()
result = append(result, "second")
wg.Done()
}()
waitForSecondToStart.Wait()
result = append(result, "first")
wg.Done()
}()
wg.Wait()
if len(result) != 2 || result[0] != "first" || result[1] != "second" {
t.Error("Second call have disresregarded a raised lock")
}
}
func Test_waitChannelWillBeEmpty_ifGateIsUnlocked(t *testing.T) {
o := NewOfflineGate()
signalingCh := o.Watch()
if signalingCh != nil {
t.Error("Signaling channel should be empty")
}
}
func Test_startWaitingForSignal_beforeGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks the gate and waits for a consumer to start up
// 2. consumer starts up, notifies main and begins waiting for the gate to be unlocked
// 3. main unlocks the gate
// 4. consumer be able to continue
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
wg := sync.WaitGroup{}
wg.Add(1)
readerIsReady := sync.WaitGroup{}
readerIsReady.Add(1)
go func(t *testing.T) {
readerIsReady.Done()
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
readerIsReady.Wait()
unlock()
wg.Wait()
}
func Test_startWaitingForSignal_afterGateGetsUnlocked(t *testing.T) {
// scenario:
// 1. main routing locks, gets waiting channel and unlocks
// 2. consumer starts up and begins waiting for the gate to be unlocked
// 3. consumer gets signal immediately and continues
o := NewOfflineGate()
unlock := o.Lock()
signalingCh := o.Watch()
unlock()
wg := sync.WaitGroup{}
wg.Add(1)
go func(t *testing.T) {
// either wait for a signal or timeout
select {
case <-signalingCh:
case <-time.After(10 * time.Second):
t.Error("Failed to wait for a signal, exit by timeout")
}
wg.Done()
}(t)
wg.Wait()
}
func Test_waitingMiddleware_executesImmediately_whenNotLocked(t *testing.T) {
// scenario:
// 1. create an gate
// 2. kick off a waiting middleware that will release immediately as gate wasn't locked
// 3. middleware shouldn't timeout
o := NewOfflineGate()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
timeout := 2 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed >= timeout {
t.Error("WaitingMiddleware had likely timeout, when it shouldn't")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
body, _ := io.ReadAll(response.Body)
if string(body) != "success" {
t.Error("Didn't receive expected result from the hanlder")
}
}
func Test_waitingMiddleware_waitsForTheLockToBeReleased(t *testing.T) {
// scenario:
// 1. create an gate and lock it
// 2. kick off a routing that will unlock the gate after 1 second
// 3. kick off a waiting middleware that will wait for lock to be eventually released
// 4. middleware shouldn't timeout
o := NewOfflineGate()
unlock := o.Lock()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
go func() {
time.Sleep(1 * time.Second)
unlock()
}()
timeout := 10 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed >= timeout {
t.Error("WaitingMiddleware had likely timeout, when it shouldn't")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
body, _ := io.ReadAll(response.Body)
if string(body) != "success" {
t.Error("Didn't receive expected result from the hanlder")
}
}
func Test_waitingMiddleware_mayTimeout_whenLockedForTooLong(t *testing.T) {
/*
scenario:
1. create an gate and lock it
2. kick off a waiting middleware that will wait for lock to be eventually released
3. because we never unlocked the gate, middleware suppose to timeout
*/
o := NewOfflineGate()
o.Lock()
request := httptest.NewRequest(http.MethodPost, "/", nil)
response := httptest.NewRecorder()
timeout := 1 * time.Second
start := time.Now()
o.WaitingMiddleware(timeout, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
elapsed := time.Since(start)
if elapsed < timeout {
t.Error("WaitingMiddleware suppose to timeout, but it didnt")
}
w.Write([]byte("success"))
})).ServeHTTP(response, request)
assert.Equal(t, http.StatusRequestTimeout, response.Result().StatusCode, "Request support to timeout waiting for the gate")
}

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors" bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
) )
@ -153,6 +153,9 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain
return nil return nil
} }
// handlers are applied backwards to the incoming request:
// - add secure handlers to the response
// - parse the JWT token and put it into the http context.
func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler { func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler {
h = bouncer.mwCheckAuthentication(h) h = bouncer.mwCheckAuthentication(h)
h = mwSecureHeaders(h) h = mwSecureHeaders(h)
@ -216,6 +219,8 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
} }
// mwCheckAuthentication provides Authentication middleware for handlers // mwCheckAuthentication provides Authentication middleware for handlers
//
// It parses the JWT token and adds the parsed token data to the http context
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler { func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var tokenData *portainer.TokenData var tokenData *portainer.TokenData
@ -269,13 +274,13 @@ func mwSecureHeaders(next http.Handler) http.Handler {
} }
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) { func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
requestContext := &RestrictedRequestContext{ if userRole == portainer.AdministratorRole {
return &RestrictedRequestContext{
IsAdmin: true, IsAdmin: true,
UserID: userID, UserID: userID,
}, nil
} }
if userRole != portainer.AdministratorRole {
requestContext.IsAdmin = false
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID) memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -288,11 +293,12 @@ func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.User
} }
} }
requestContext.IsTeamLeader = isTeamLeader return &RestrictedRequestContext{
requestContext.UserMemberships = memberships IsAdmin: false,
} UserID: userID,
IsTeamLeader: isTeamLeader,
return requestContext, nil UserMemberships: memberships,
}, nil
} }
// EdgeComputeOperation defines a restriced edge compute operation. // EdgeComputeOperation defines a restriced edge compute operation.

View File

@ -1,15 +1,20 @@
package http package http
import ( import (
"context"
"fmt"
"log"
"net/http" "net/http"
"path/filepath" "path/filepath"
"time" "time"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth" "github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates" "github.com/portainer/portainer/api/http/handler/customtemplates"
"github.com/portainer/portainer/api/http/handler/dockerhub" "github.com/portainer/portainer/api/http/handler/dockerhub"
"github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgegroups"
@ -36,10 +41,10 @@ import (
"github.com/portainer/portainer/api/http/handler/users" "github.com/portainer/portainer/api/http/handler/users"
"github.com/portainer/portainer/api/http/handler/webhooks" "github.com/portainer/portainer/api/http/handler/webhooks"
"github.com/portainer/portainer/api/http/handler/websocket" "github.com/portainer/portainer/api/http/handler/websocket"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/kubernetes/cli"
) )
@ -69,6 +74,8 @@ type Server struct {
DockerClientFactory *docker.ClientFactory DockerClientFactory *docker.ClientFactory
KubernetesClientFactory *cli.ClientFactory KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer KubernetesDeployer portainer.KubernetesDeployer
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc
} }
// Start starts the HTTP server // Start starts the HTTP server
@ -78,6 +85,7 @@ func (server *Server) Start() error {
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService) requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour) rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
offlineGate := offlinegate.NewOfflineGate()
var authHandler = auth.NewHandler(requestBouncer, rateLimiter) var authHandler = auth.NewHandler(requestBouncer, rateLimiter)
authHandler.DataStore = server.DataStore authHandler.DataStore = server.DataStore
@ -88,6 +96,11 @@ func (server *Server) Start() error {
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
authHandler.OAuthService = server.OAuthService authHandler.OAuthService = server.OAuthService
adminMonitor := adminmonitor.New(5*time.Minute, server.DataStore, server.ShutdownCtx)
adminMonitor.Start()
var backupHandler = backup.NewHandler(requestBouncer, server.DataStore, offlineGate, server.FileService.GetDatastorePath(), server.ShutdownTrigger, adminMonitor)
var roleHandler = roles.NewHandler(requestBouncer) var roleHandler = roles.NewHandler(requestBouncer)
roleHandler.DataStore = server.DataStore roleHandler.DataStore = server.DataStore
@ -200,6 +213,7 @@ func (server *Server) Start() error {
server.Handler = &handler.Handler{ server.Handler = &handler.Handler{
RoleHandler: roleHandler, RoleHandler: roleHandler,
AuthHandler: authHandler, AuthHandler: authHandler,
BackupHandler: backupHandler,
CustomTemplatesHandler: customTemplatesHandler, CustomTemplatesHandler: customTemplatesHandler,
DockerHubHandler: dockerHubHandler, DockerHubHandler: dockerHubHandler,
EdgeGroupsHandler: edgeGroupsHandler, EdgeGroupsHandler: edgeGroupsHandler,
@ -231,10 +245,27 @@ func (server *Server) Start() error {
Addr: server.BindAddress, Addr: server.BindAddress,
Handler: server.Handler, Handler: server.Handler,
} }
httpServer.Handler = offlineGate.WaitingMiddleware(time.Minute, httpServer.Handler)
if server.SSL { if server.SSL {
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration() httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey) return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
} }
go server.shutdown(httpServer)
return httpServer.ListenAndServe() return httpServer.ListenAndServe()
} }
func (server *Server) shutdown(httpServer *http.Server) {
<-server.ShutdownCtx.Done()
log.Println("[DEBUG] Shutting down http server")
shutdownTimeout, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := httpServer.Shutdown(shutdownTimeout)
if err != nil {
fmt.Printf("Failed shutdown http server: %s \n", err)
}
}

View File

@ -0,0 +1,19 @@
package edge
import portainer "github.com/portainer/portainer/api"
// LoadEdgeJobs registers all edge jobs inside corresponding endpoint tunnel
func LoadEdgeJobs(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error {
edgeJobs, err := dataStore.EdgeJob().EdgeJobs()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointID := range edgeJob.Endpoints {
reverseTunnelService.AddEdgeJob(endpointID, &edgeJob)
}
}
return nil
}

View File

@ -1,6 +1,7 @@
package snapshot package snapshot
import ( import (
"context"
"log" "log"
"time" "time"
@ -16,10 +17,11 @@ type Service struct {
snapshotIntervalInSeconds float64 snapshotIntervalInSeconds float64
dockerSnapshotter portainer.DockerSnapshotter dockerSnapshotter portainer.DockerSnapshotter
kubernetesSnapshotter portainer.KubernetesSnapshotter kubernetesSnapshotter portainer.KubernetesSnapshotter
shutdownCtx context.Context
} }
// NewService creates a new instance of a service // NewService creates a new instance of a service
func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter) (*Service, error) { func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter, shutdownCtx context.Context) (*Service, error) {
snapshotFrequency, err := time.ParseDuration(snapshotInterval) snapshotFrequency, err := time.ParseDuration(snapshotInterval)
if err != nil { if err != nil {
return nil, err return nil, err
@ -30,6 +32,7 @@ func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSn
snapshotIntervalInSeconds: snapshotFrequency.Seconds(), snapshotIntervalInSeconds: snapshotFrequency.Seconds(),
dockerSnapshotter: dockerSnapshotter, dockerSnapshotter: dockerSnapshotter,
kubernetesSnapshotter: kubernetesSnapshotter, kubernetesSnapshotter: kubernetesSnapshotter,
shutdownCtx: shutdownCtx,
}, nil }, nil
} }
@ -43,7 +46,7 @@ func (service *Service) Start() {
service.startSnapshotLoop() service.startSnapshotLoop()
} }
func (service *Service) stop() { func (service *Service) Stop() {
if service.refreshSignal == nil { if service.refreshSignal == nil {
return return
} }
@ -55,7 +58,7 @@ func (service *Service) stop() {
// SetSnapshotInterval sets the snapshot interval and resets the service // SetSnapshotInterval sets the snapshot interval and resets the service
func (service *Service) SetSnapshotInterval(snapshotInterval string) error { func (service *Service) SetSnapshotInterval(snapshotInterval string) error {
service.stop() service.Stop()
snapshotFrequency, err := time.ParseDuration(snapshotInterval) snapshotFrequency, err := time.ParseDuration(snapshotInterval)
if err != nil { if err != nil {
@ -132,9 +135,12 @@ func (service *Service) startSnapshotLoop() error {
if err != nil { if err != nil {
log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (endpoint snapshot).] [error: %s]", err) log.Printf("[ERROR] [internal,snapshot] [message: background schedule error (endpoint snapshot).] [error: %s]", err)
} }
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
ticker.Stop()
return
case <-service.refreshSignal: case <-service.refreshSignal:
log.Println("[DEBUG] [internal,snapshot] [message: shutting down Snapshot service]") log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
ticker.Stop() ticker.Stop()
return return
} }

View File

@ -0,0 +1,114 @@
package testhelpers
import (
"io"
portainer "github.com/portainer/portainer/api"
)
type datastore struct {
dockerHub portainer.DockerHubService
customTemplate portainer.CustomTemplateService
edgeGroup portainer.EdgeGroupService
edgeJob portainer.EdgeJobService
edgeStack portainer.EdgeStackService
endpoint portainer.EndpointService
endpointGroup portainer.EndpointGroupService
endpointRelation portainer.EndpointRelationService
registry portainer.RegistryService
resourceControl portainer.ResourceControlService
role portainer.RoleService
settings portainer.SettingsService
stack portainer.StackService
tag portainer.TagService
teamMembership portainer.TeamMembershipService
team portainer.TeamService
tunnelServer portainer.TunnelServerService
user portainer.UserService
version portainer.VersionService
webhook portainer.WebhookService
}
func (d *datastore) BackupTo(io.Writer) error { return nil }
func (d *datastore) Open() error { return nil }
func (d *datastore) Init() error { return nil }
func (d *datastore) Close() error { return nil }
func (d *datastore) CheckCurrentEdition() error { return nil }
func (d *datastore) IsNew() bool { return false }
func (d *datastore) MigrateData(force bool) error { return nil }
func (d *datastore) RollbackToCE() error { return nil }
func (d *datastore) DockerHub() portainer.DockerHubService { return d.dockerHub }
func (d *datastore) CustomTemplate() portainer.CustomTemplateService { return d.customTemplate }
func (d *datastore) EdgeGroup() portainer.EdgeGroupService { return d.edgeGroup }
func (d *datastore) EdgeJob() portainer.EdgeJobService { return d.edgeJob }
func (d *datastore) EdgeStack() portainer.EdgeStackService { return d.edgeStack }
func (d *datastore) Endpoint() portainer.EndpointService { return d.endpoint }
func (d *datastore) EndpointGroup() portainer.EndpointGroupService { return d.endpointGroup }
func (d *datastore) EndpointRelation() portainer.EndpointRelationService { return d.endpointRelation }
func (d *datastore) Registry() portainer.RegistryService { return d.registry }
func (d *datastore) ResourceControl() portainer.ResourceControlService { return d.resourceControl }
func (d *datastore) Role() portainer.RoleService { return d.role }
func (d *datastore) Settings() portainer.SettingsService { return d.settings }
func (d *datastore) Stack() portainer.StackService { return d.stack }
func (d *datastore) Tag() portainer.TagService { return d.tag }
func (d *datastore) TeamMembership() portainer.TeamMembershipService { return d.teamMembership }
func (d *datastore) Team() portainer.TeamService { return d.team }
func (d *datastore) TunnelServer() portainer.TunnelServerService { return d.tunnelServer }
func (d *datastore) User() portainer.UserService { return d.user }
func (d *datastore) Version() portainer.VersionService { return d.version }
func (d *datastore) Webhook() portainer.WebhookService { return d.webhook }
type datastoreOption = func(d *datastore)
// NewDatastore creates new instance of datastore.
// Will apply options before returning, opts will be applied from left to right.
func NewDatastore(options ...datastoreOption) *datastore {
d := datastore{}
for _, o := range options {
o(&d)
}
return &d
}
type stubUserService struct {
users []portainer.User
}
func (s *stubUserService) User(ID portainer.UserID) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) UserByUsername(username string) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) Users() ([]portainer.User, error) { return s.users, nil }
func (s *stubUserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
return s.users, nil
}
func (s *stubUserService) CreateUser(user *portainer.User) error { return nil }
func (s *stubUserService) UpdateUser(ID portainer.UserID, user *portainer.User) error { return nil }
func (s *stubUserService) DeleteUser(ID portainer.UserID) error { return nil }
// WithUsers datastore option that will instruct datastore to return provided users
func WithUsers(us []portainer.User) datastoreOption {
return func(d *datastore) {
d.user = &stubUserService{users: us}
}
}
type stubEdgeJobService struct {
jobs []portainer.EdgeJob
}
func (s *stubEdgeJobService) EdgeJobs() ([]portainer.EdgeJob, error) { return s.jobs, nil }
func (s *stubEdgeJobService) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
return nil, nil
}
func (s *stubEdgeJobService) CreateEdgeJob(edgeJob *portainer.EdgeJob) error { return nil }
func (s *stubEdgeJobService) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
return nil
}
func (s *stubEdgeJobService) DeleteEdgeJob(ID portainer.EdgeJobID) error { return nil }
func (s *stubEdgeJobService) GetNextIdentifier() int { return 0 }
// WithEdgeJobs option will instruct datastore to return provided jobs
func WithEdgeJobs(js []portainer.EdgeJob) datastoreOption {
return func(d *datastore) {
d.edgeJob = &stubEdgeJobService{jobs: js}
}
}

View File

@ -0,0 +1,23 @@
package testhelpers
import portainer "github.com/portainer/portainer/api"
type ReverseTunnelService struct{}
func (r ReverseTunnelService) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
return nil
}
func (r ReverseTunnelService) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
return "nil"
}
func (r ReverseTunnelService) SetTunnelStatusToActive(endpointID portainer.EndpointID) {}
func (r ReverseTunnelService) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
return nil
}
func (r ReverseTunnelService) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {}
func (r ReverseTunnelService) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
return nil
}
func (r ReverseTunnelService) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
}
func (r ReverseTunnelService) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {}

View File

@ -1000,8 +1000,9 @@ type (
Init() error Init() error
Close() error Close() error
IsNew() bool IsNew() bool
MigrateData() error MigrateData(force bool) error
CheckCurrentEdition() error CheckCurrentEdition() error
BackupTo(w io.Writer) error
DockerHub() DockerHubService DockerHub() DockerHubService
CustomTemplate() CustomTemplateService CustomTemplate() CustomTemplateService
@ -1130,6 +1131,7 @@ type (
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
GetCustomTemplateProjectPath(identifier string) string GetCustomTemplateProjectPath(identifier string) string
GetTemporaryPath() (string, error) GetTemporaryPath() (string, error)
GetDatastorePath() string
} }
// GitService represents a service for managing Git // GitService represents a service for managing Git
@ -1196,6 +1198,7 @@ type (
// ReverseTunnelService represents a service used to manage reverse tunnel connections. // ReverseTunnelService represents a service used to manage reverse tunnel connections.
ReverseTunnelService interface { ReverseTunnelService interface {
StartTunnelServer(addr, port string, snapshotService SnapshotService) error StartTunnelServer(addr, port string, snapshotService SnapshotService) error
StopTunnelServer() error
GenerateEdgeKey(url, host string, endpointIdentifier int) string GenerateEdgeKey(url, host string, endpointIdentifier int) string
SetTunnelStatusToActive(endpointID EndpointID) SetTunnelStatusToActive(endpointID EndpointID)
SetTunnelStatusToRequired(endpointID EndpointID) error SetTunnelStatusToRequired(endpointID EndpointID) error
@ -1238,6 +1241,7 @@ type (
// SnapshotService represents a service for managing endpoint snapshots // SnapshotService represents a service for managing endpoint snapshots
SnapshotService interface { SnapshotService interface {
Start() Start()
Stop()
SetSnapshotInterval(snapshotInterval string) error SetSnapshotInterval(snapshotInterval string) error
SnapshotEndpoint(endpoint *Endpoint) error SnapshotEndpoint(endpoint *Endpoint) error
} }