mirror of https://github.com/portainer/portainer
Merge pull request #4965 from portainer/feat(backup)-backup-restore-system
feat(backup): Add backup/restore to the server [EE-386] [EE-378] [CE-452]pull/5007/head
commit
335bfb81ba
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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")
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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__ os.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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"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, _ := ioutils.TempDir("", "backup")
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
err := copyFile("does-not-exist", tmpdir)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
|
||||
tmpdir, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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"))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -2,7 +2,7 @@ package customtemplate
|
|||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing custom template data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -56,7 +56,7 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
|
|||
var customTemplate portainer.CustomTemplate
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &customTemplate)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &customTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -67,18 +67,18 @@ func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portaine
|
|||
// UpdateCustomTemplate updates an custom template.
|
||||
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
|
||||
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.
|
||||
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
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.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
|
@ -42,7 +44,7 @@ const (
|
|||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
path string
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
isNew bool
|
||||
fileService portainer.FileService
|
||||
CustomTemplateService *customtemplate.Service
|
||||
|
@ -83,6 +85,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro
|
|||
path: storePath,
|
||||
fileService: fileService,
|
||||
isNew: true,
|
||||
connection: &internal.DbConnection{},
|
||||
}
|
||||
|
||||
databasePath := path.Join(storePath, databaseFileName)
|
||||
|
@ -105,15 +108,15 @@ func (store *Store) Open() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.db = db
|
||||
store.connection.DB = db
|
||||
|
||||
return store.initServices()
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
func (store *Store) Close() error {
|
||||
if store.db != nil {
|
||||
return store.db.Close()
|
||||
if store.connection.DB != nil {
|
||||
return store.connection.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -134,8 +137,9 @@ func (store *Store) CheckCurrentEdition() error {
|
|||
|
||||
// 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.
|
||||
func (store *Store) MigrateData() error {
|
||||
if store.isNew {
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
|
@ -148,7 +152,7 @@ func (store *Store) MigrateData() error {
|
|||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.db,
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
|
@ -180,238 +184,11 @@ func (store *Store) MigrateData() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) initServices() error {
|
||||
authorizationsetService, err := role.NewService(store.db)
|
||||
if err != nil {
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (store *Store) BackupTo(w io.Writer) error {
|
||||
return store.connection.View(func(tx *bolt.Tx) error {
|
||||
_, err := tx.WriteTo(w)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package dockerhub
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -15,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing Dockerhub data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) DockerHub() (*portainer.DockerHub, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -44,5 +42,5 @@ func (service *Service) DockerHub() (*portainer.DockerHub, error) {
|
|||
|
||||
// UpdateDockerHub updates a DockerHub object.
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package edgegroup
|
|||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing Edge group data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -56,7 +56,7 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
|
|||
var group portainer.EdgeGroup
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &group)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -67,18 +67,18 @@ func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGrou
|
|||
// UpdateEdgeGroup updates an Edge group.
|
||||
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||
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.
|
||||
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
|
|
@ -2,7 +2,7 @@ package edgejob
|
|||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing edge jobs data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -56,7 +56,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
|
|||
var edgeJob portainer.EdgeJob
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &edgeJob)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &edgeJob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
|
|||
|
||||
// CreateEdgeJob creates a new Edge job
|
||||
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))
|
||||
|
||||
if edgeJob.ID == 0 {
|
||||
|
@ -86,16 +86,16 @@ func (service *Service) CreateEdgeJob(edgeJob *portainer.EdgeJob) error {
|
|||
// UpdateEdgeJob updates an Edge job by ID
|
||||
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
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
|
||||
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
||||
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.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package edgestack
|
|||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing Edge stack data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -56,7 +56,7 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
|
|||
var stack portainer.EdgeStack
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &stack)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
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.
|
||||
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))
|
||||
|
||||
if edgeStack.ID == 0 {
|
||||
|
@ -86,16 +86,16 @@ func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
|
|||
// UpdateEdgeStack updates an Edge stack.
|
||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
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.
|
||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
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.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package endpoint
|
|||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
|
|||
var endpoint portainer.Endpoint
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &endpoint)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -44,20 +44,20 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
|
|||
// UpdateEndpoint updates an endpoint.
|
||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
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.
|
||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
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.
|
||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
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))
|
||||
|
||||
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.
|
||||
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))
|
||||
|
||||
// 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.
|
||||
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.
|
||||
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))
|
||||
|
||||
for _, endpoint := range toCreate {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package endpointgroup
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
|
|||
var endpointGroup portainer.EndpointGroup
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &endpointGroup)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &endpointGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -45,20 +45,20 @@ func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.
|
|||
// UpdateEndpointGroup updates an endpoint group.
|
||||
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
||||
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.
|
||||
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
||||
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.
|
||||
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||
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))
|
||||
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
|
|
@ -13,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint relation data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
|
|||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := internal.Itob(int(endpointID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &endpointRelation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
|
|||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
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))
|
||||
|
||||
data, err := internal.MarshalObject(endpointRelation)
|
||||
|
@ -58,11 +58,11 @@ func (service *Service) CreateEndpointRelation(endpointRelation *portainer.Endpo
|
|||
// UpdateEndpointRelation updates an Endpoint relation object
|
||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
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
|
||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
||||
identifier := internal.Itob(int(EndpointID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
|
|||
var extension portainer.Extension
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &extension)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
|
|||
func (service *Service) Extensions() ([]portainer.Extension, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -67,7 +67,7 @@ func (service *Service) Extensions() ([]portainer.Extension, error) {
|
|||
|
||||
// Persist persists a extension inside the database.
|
||||
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))
|
||||
|
||||
data, err := internal.MarshalObject(extension)
|
||||
|
@ -82,5 +82,5 @@ func (service *Service) Persist(extension *portainer.Extension) error {
|
|||
// DeleteExtension deletes a Extension.
|
||||
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import (
|
|||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type DbConnection struct {
|
||||
*bolt.DB
|
||||
}
|
||||
|
||||
// Itob returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// 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.
|
||||
func CreateBucket(db *bolt.DB, bucketName string) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
func CreateBucket(connection *DbConnection, bucketName string) error {
|
||||
return connection.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
if err != nil {
|
||||
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.
|
||||
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
|
||||
|
||||
err := db.View(func(tx *bolt.Tx) error {
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
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.
|
||||
func UpdateObject(db *bolt.DB, bucketName string, key []byte, object interface{}) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
func UpdateObject(connection *DbConnection, bucketName string, key []byte, object interface{}) error {
|
||||
return connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
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.
|
||||
func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
func DeleteObject(connection *DbConnection, bucketName string, key []byte) error {
|
||||
return connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
return bucket.Delete(key)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
|
|||
var registry portainer.Registry
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, ®istry)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry,
|
|||
func (service *Service) Registries() ([]portainer.Registry, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -67,7 +67,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
|
|||
|
||||
// CreateRegistry creates a new registry.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -85,11 +85,11 @@ func (service *Service) CreateRegistry(registry *portainer.Registry) error {
|
|||
// UpdateRegistry updates an registry.
|
||||
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
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.
|
||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package resourcecontrol
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
|||
var resourceControl portainer.ResourceControl
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &resourceControl)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &resourceControl)
|
||||
if err != nil {
|
||||
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) {
|
||||
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))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
|
@ -82,7 +82,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
|
|||
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -103,7 +103,7 @@ func (service *Service) ResourceControls() ([]portainer.ResourceControl, error)
|
|||
|
||||
// CreateResourceControl creates a new ResourceControl object
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -121,11 +121,11 @@ func (service *Service) CreateResourceControl(resourceControl *portainer.Resourc
|
|||
// UpdateResourceControl saves a ResourceControl object.
|
||||
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
|
||||
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
|
||||
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package role
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
|||
var set portainer.Role
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &set)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
|||
func (service *Service) Roles() ([]portainer.Role, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -67,7 +67,7 @@ func (service *Service) Roles() ([]portainer.Role, error) {
|
|||
|
||||
// CreateRole creates a new Role.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -85,5 +85,5 @@ func (service *Service) CreateRole(role *portainer.Role) error {
|
|||
// UpdateRole updates a role.
|
||||
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, role)
|
||||
return internal.UpdateObject(service.connection, BucketName, identifier, role)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package schedule
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing schedule data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
|
|||
var schedule portainer.Schedule
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &schedule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -45,20 +45,20 @@ func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule,
|
|||
// UpdateSchedule updates a schedule.
|
||||
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
|
||||
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.
|
||||
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
|
||||
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.
|
||||
func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -82,7 +82,7 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
|||
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
|
||||
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))
|
||||
|
||||
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.
|
||||
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))
|
||||
|
||||
// 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.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,10 +1,8 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -15,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -44,5 +42,5 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
|||
|
||||
// UpdateSettings persists a Settings object.
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package stack
|
||||
|
||||
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/internal"
|
||||
|
||||
|
@ -15,18 +15,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
|
|||
var stack portainer.Stack
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &stack)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
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) {
|
||||
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))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
|
@ -78,7 +78,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
|||
func (service *Service) Stacks() ([]portainer.Stack, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -99,12 +99,12 @@ func (service *Service) Stacks() ([]portainer.Stack, error) {
|
|||
|
||||
// GetNextIdentifier returns the next identifier for a stack.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
return internal.GetNextIdentifier(service.connection, BucketName)
|
||||
}
|
||||
|
||||
// CreateStack creates a new stack.
|
||||
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))
|
||||
|
||||
// We manually manage sequences for stacks
|
||||
|
@ -125,11 +125,11 @@ func (service *Service) CreateStack(stack *portainer.Stack) error {
|
|||
// UpdateStack updates a stack.
|
||||
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
|
||||
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.
|
||||
func (service *Service) DeleteStack(ID portainer.StackID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) Tags() ([]portainer.Tag, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -57,7 +57,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
|
|||
var tag portainer.Tag
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &tag)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
|
|||
|
||||
// CreateTag creates a new tag.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -85,11 +85,11 @@ func (service *Service) CreateTag(tag *portainer.Tag) error {
|
|||
// UpdateTag updates a tag.
|
||||
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
|
||||
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.
|
||||
func (service *Service) DeleteTag(ID portainer.TagID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -17,18 +17,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ func (service *Service) Team(ID portainer.TeamID) (*portainer.Team, error) {
|
|||
var team portainer.Team
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &team)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &team)
|
||||
if err != nil {
|
||||
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) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -80,7 +80,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
|||
func (service *Service) Teams() ([]portainer.Team, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -102,12 +102,12 @@ func (service *Service) Teams() ([]portainer.Team, error) {
|
|||
// UpdateTeam saves a Team.
|
||||
func (service *Service) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -125,5 +125,5 @@ func (service *Service) CreateTeam(team *portainer.Team) error {
|
|||
// DeleteTeam deletes a Team.
|
||||
func (service *Service) DeleteTeam(ID portainer.TeamID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package teammembership
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
@ -14,18 +14,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
|
|||
var membership portainer.TeamMembership
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &membership)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &membership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portaine
|
|||
func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -69,7 +69,7 @@ func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
|
|||
func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||
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))
|
||||
|
||||
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) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -120,12 +120,12 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
|
|||
// UpdateTeamMembership saves a TeamMembership object.
|
||||
func (service *Service) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -143,12 +143,12 @@ func (service *Service) CreateTeamMembership(membership *portainer.TeamMembershi
|
|||
// DeleteTeamMembership deletes a TeamMembership object.
|
||||
func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
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.
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package tunnelserver
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -15,18 +13,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +32,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -44,5 +42,5 @@ func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
|
|||
|
||||
// UpdateInfo persists a TunnelServerInfo object.
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -17,18 +17,18 @@ const (
|
|||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
|
|||
var user portainer.User
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &user)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
|||
|
||||
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))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
|
@ -81,7 +81,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
|||
func (service *Service) Users() ([]portainer.User, error) {
|
||||
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))
|
||||
|
||||
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.
|
||||
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
|
||||
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))
|
||||
|
||||
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 {
|
||||
identifier := internal.Itob(int(ID))
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
@ -152,5 +152,5 @@ func (service *Service) CreateUser(user *portainer.User) error {
|
|||
// DeleteUser deletes a user.
|
||||
func (service *Service) DeleteUser(ID portainer.UserID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
return internal.DeleteObject(service.connection, BucketName, identifier)
|
||||
}
|
||||
|
|
|
@ -19,18 +19,18 @@ const (
|
|||
|
||||
// Service represents a service to manage stored versions.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) DBVersion() (int, error) {
|
||||
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))
|
||||
|
||||
value := bucket.Get([]byte(versionKey))
|
||||
|
@ -75,7 +75,7 @@ func (service *Service) Edition() (portainer.SoftwareEdition, error) {
|
|||
|
||||
// StoreDBVersion store the database version.
|
||||
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))
|
||||
|
||||
data := []byte(strconv.Itoa(version))
|
||||
|
@ -87,7 +87,7 @@ func (service *Service) StoreDBVersion(version int) error {
|
|||
func (service *Service) InstanceID() (string, error) {
|
||||
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))
|
||||
|
||||
value := bucket.Get([]byte(instanceKey))
|
||||
|
@ -109,7 +109,7 @@ func (service *Service) InstanceID() (string, error) {
|
|||
|
||||
// StoreInstanceID store the instance ID.
|
||||
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))
|
||||
|
||||
data := []byte(ID)
|
||||
|
@ -120,7 +120,7 @@ func (service *Service) StoreInstanceID(ID string) error {
|
|||
func (service *Service) getKey(key string) ([]byte, error) {
|
||||
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))
|
||||
|
||||
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 {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
return service.connection.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data := []byte(value)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package webhook
|
||||
|
||||
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/internal"
|
||||
|
||||
|
@ -15,18 +15,18 @@ const (
|
|||
|
||||
// Service represents a service for managing webhook data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
connection *internal.DbConnection
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
func NewService(connection *internal.DbConnection) (*Service, error) {
|
||||
err := internal.CreateBucket(connection, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ func NewService(db *bolt.DB) (*Service, error) {
|
|||
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
|
||||
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))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
|
@ -58,7 +58,7 @@ func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, err
|
|||
var webhook portainer.Webhook
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &webhook)
|
||||
err := internal.GetObject(service.connection, BucketName, identifier, &webhook)
|
||||
if err != nil {
|
||||
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) {
|
||||
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))
|
||||
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) {
|
||||
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))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
|
@ -131,12 +131,12 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
|
|||
// DeleteWebhook deletes a webhook.
|
||||
func (service *Service) DeleteWebhook(ID portainer.WebhookID) error {
|
||||
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.
|
||||
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))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package chisel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
@ -9,7 +10,7 @@ import (
|
|||
"github.com/dchest/uniuri"
|
||||
chserver "github.com/jpillora/chisel/server"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
|
@ -29,13 +30,15 @@ type Service struct {
|
|||
dataStore portainer.DataStore
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
}
|
||||
|
||||
// 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{
|
||||
tunnelDetailsMap: cmap.New(),
|
||||
dataStore: dataStore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,6 +86,11 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
|
|||
return nil
|
||||
}
|
||||
|
||||
// StopTunnelServer stops tunnel http server
|
||||
func (service *Service) StopTunnelServer() error {
|
||||
return service.chiselServer.Close()
|
||||
}
|
||||
|
||||
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
||||
var serverInfo *portainer.TunnelServerInfo
|
||||
|
||||
|
@ -108,13 +116,16 @@ func (service *Service) retrievePrivateKeySeed() (string, error) {
|
|||
func (service *Service) startTunnelVerificationLoop() {
|
||||
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
|
||||
ticker := time.NewTicker(tunnelCleanupInterval)
|
||||
stopSignal := make(chan struct{})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
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/jwt"
|
||||
"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)
|
||||
}
|
||||
|
||||
err = store.MigrateData()
|
||||
err = store.MigrateData(false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed migration: %v", err)
|
||||
}
|
||||
|
@ -136,11 +137,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
|
|||
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)
|
||||
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
|
||||
|
||||
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter)
|
||||
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -148,21 +149,6 @@ func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore,
|
|||
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 {
|
||||
return &portainer.Status{
|
||||
Version: portainer.APIVersion,
|
||||
|
@ -353,28 +339,12 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
|
|||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
||||
}
|
||||
|
||||
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
|
||||
timer1 := time.NewTimer(5 * time.Minute)
|
||||
<-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()
|
||||
func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, fileService)
|
||||
defer dataStore.Close()
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -400,7 +370,7 @@ func main() {
|
|||
log.Fatalf("failed initializing key pai: %v", err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
|
@ -410,7 +380,7 @@ func main() {
|
|||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
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,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
|
@ -513,11 +481,18 @@ func main() {
|
|||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("failed starting server: %v", err)
|
||||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
tmpdir, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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")
|
||||
}
|
|
@ -9,7 +9,7 @@ import (
|
|||
"io/ioutil"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
|
@ -505,3 +505,8 @@ func (service *Service) GetTemporaryPath() (string, error) {
|
|||
|
||||
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
|
||||
}
|
||||
|
||||
// GetDataStorePath returns path to data folder
|
||||
func (service *Service) GetDatastorePath() string {
|
||||
return service.dataStorePath
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ require (
|
|||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
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/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
|
|
|
@ -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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
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 v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"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, _ := ioutils.TempDir("", "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, _ := ioutils.TempDir("", "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"))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -0,0 +1 @@
|
|||
content
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"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/dockerhub"
|
||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||
|
@ -36,6 +37,7 @@ import (
|
|||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *auth.Handler
|
||||
BackupHandler *backup.Handler
|
||||
CustomTemplatesHandler *customtemplates.Handler
|
||||
DockerHubHandler *dockerhub.Handler
|
||||
EdgeGroupsHandler *edgegroups.Handler
|
||||
|
@ -140,6 +142,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||
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"):
|
||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/custom_templates"):
|
||||
|
|
|
@ -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)
|
||||
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
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"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
@ -153,6 +153,9 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain
|
|||
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 {
|
||||
h = bouncer.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
|
@ -216,6 +219,8 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenData *portainer.TokenData
|
||||
|
@ -269,30 +274,31 @@ func mwSecureHeaders(next http.Handler) http.Handler {
|
|||
}
|
||||
|
||||
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
||||
requestContext := &RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
UserID: userID,
|
||||
if userRole == portainer.AdministratorRole {
|
||||
return &RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
UserID: userID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if userRole != portainer.AdministratorRole {
|
||||
requestContext.IsAdmin = false
|
||||
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isTeamLeader := false
|
||||
for _, membership := range memberships {
|
||||
if membership.Role == portainer.TeamLeader {
|
||||
isTeamLeader = true
|
||||
}
|
||||
}
|
||||
|
||||
requestContext.IsTeamLeader = isTeamLeader
|
||||
requestContext.UserMemberships = memberships
|
||||
memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return requestContext, nil
|
||||
isTeamLeader := false
|
||||
for _, membership := range memberships {
|
||||
if membership.Role == portainer.TeamLeader {
|
||||
isTeamLeader = true
|
||||
}
|
||||
}
|
||||
|
||||
return &RestrictedRequestContext{
|
||||
IsAdmin: false,
|
||||
UserID: userID,
|
||||
IsTeamLeader: isTeamLeader,
|
||||
UserMemberships: memberships,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EdgeComputeOperation defines a restriced edge compute operation.
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/handler"
|
||||
"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/dockerhub"
|
||||
"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/webhooks"
|
||||
"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/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
|
@ -69,6 +74,8 @@ type Server struct {
|
|||
DockerClientFactory *docker.ClientFactory
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
KubernetesDeployer portainer.KubernetesDeployer
|
||||
ShutdownCtx context.Context
|
||||
ShutdownTrigger context.CancelFunc
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -78,6 +85,7 @@ func (server *Server) Start() error {
|
|||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
offlineGate := offlinegate.NewOfflineGate()
|
||||
|
||||
var authHandler = auth.NewHandler(requestBouncer, rateLimiter)
|
||||
authHandler.DataStore = server.DataStore
|
||||
|
@ -88,6 +96,11 @@ func (server *Server) Start() error {
|
|||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||
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)
|
||||
roleHandler.DataStore = server.DataStore
|
||||
|
||||
|
@ -200,6 +213,7 @@ func (server *Server) Start() error {
|
|||
server.Handler = &handler.Handler{
|
||||
RoleHandler: roleHandler,
|
||||
AuthHandler: authHandler,
|
||||
BackupHandler: backupHandler,
|
||||
CustomTemplatesHandler: customTemplatesHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
EdgeGroupsHandler: edgeGroupsHandler,
|
||||
|
@ -231,10 +245,27 @@ func (server *Server) Start() error {
|
|||
Addr: server.BindAddress,
|
||||
Handler: server.Handler,
|
||||
}
|
||||
httpServer.Handler = offlineGate.WaitingMiddleware(time.Minute, httpServer.Handler)
|
||||
|
||||
if server.SSL {
|
||||
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
|
||||
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
|
||||
}
|
||||
|
||||
go server.shutdown(httpServer)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
|
@ -16,10 +17,11 @@ type Service struct {
|
|||
snapshotIntervalInSeconds float64
|
||||
dockerSnapshotter portainer.DockerSnapshotter
|
||||
kubernetesSnapshotter portainer.KubernetesSnapshotter
|
||||
shutdownCtx context.Context
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -30,6 +32,7 @@ func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSn
|
|||
snapshotIntervalInSeconds: snapshotFrequency.Seconds(),
|
||||
dockerSnapshotter: dockerSnapshotter,
|
||||
kubernetesSnapshotter: kubernetesSnapshotter,
|
||||
shutdownCtx: shutdownCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,7 @@ func (service *Service) Start() {
|
|||
service.startSnapshotLoop()
|
||||
}
|
||||
|
||||
func (service *Service) stop() {
|
||||
func (service *Service) Stop() {
|
||||
if service.refreshSignal == nil {
|
||||
return
|
||||
}
|
||||
|
@ -55,7 +58,7 @@ func (service *Service) stop() {
|
|||
|
||||
// SetSnapshotInterval sets the snapshot interval and resets the service
|
||||
func (service *Service) SetSnapshotInterval(snapshotInterval string) error {
|
||||
service.stop()
|
||||
service.Stop()
|
||||
|
||||
snapshotFrequency, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
|
@ -132,9 +135,12 @@ func (service *Service) startSnapshotLoop() error {
|
|||
if err != nil {
|
||||
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:
|
||||
log.Println("[DEBUG] [internal,snapshot] [message: shutting down Snapshot service]")
|
||||
log.Println("[DEBUG] [internal,snapshot] [message: shutting down snapshotting]")
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -383,8 +383,8 @@ type (
|
|||
|
||||
// QuayRegistryData represents data required for Quay registry to work
|
||||
QuayRegistryData struct {
|
||||
UseOrganisation bool `json:"UseOrganisation"`
|
||||
OrganisationName string `json:"OrganisationName"`
|
||||
UseOrganisation bool `json:"UseOrganisation"`
|
||||
OrganisationName string `json:"OrganisationName"`
|
||||
}
|
||||
|
||||
// JobType represents a job type
|
||||
|
@ -1002,8 +1002,9 @@ type (
|
|||
Init() error
|
||||
Close() error
|
||||
IsNew() bool
|
||||
MigrateData() error
|
||||
MigrateData(force bool) error
|
||||
CheckCurrentEdition() error
|
||||
BackupTo(w io.Writer) error
|
||||
|
||||
DockerHub() DockerHubService
|
||||
CustomTemplate() CustomTemplateService
|
||||
|
@ -1132,6 +1133,7 @@ type (
|
|||
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
|
||||
GetCustomTemplateProjectPath(identifier string) string
|
||||
GetTemporaryPath() (string, error)
|
||||
GetDatastorePath() string
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
|
@ -1198,6 +1200,7 @@ type (
|
|||
// ReverseTunnelService represents a service used to manage reverse tunnel connections.
|
||||
ReverseTunnelService interface {
|
||||
StartTunnelServer(addr, port string, snapshotService SnapshotService) error
|
||||
StopTunnelServer() error
|
||||
GenerateEdgeKey(url, host string, endpointIdentifier int) string
|
||||
SetTunnelStatusToActive(endpointID EndpointID)
|
||||
SetTunnelStatusToRequired(endpointID EndpointID) error
|
||||
|
@ -1240,6 +1243,7 @@ type (
|
|||
// SnapshotService represents a service for managing endpoint snapshots
|
||||
SnapshotService interface {
|
||||
Start()
|
||||
Stop()
|
||||
SetSnapshotInterval(snapshotInterval string) error
|
||||
SnapshotEndpoint(endpoint *Endpoint) error
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ angular
|
|||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
|
||||
.constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks')
|
||||
.constant('API_ENDPOINT_BACKUP', 'api/backup')
|
||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('APPLICATION_CACHE_VALIDITY', 3600)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
angular.module('portainer.app').factory('Backup', [
|
||||
'$resource',
|
||||
'API_ENDPOINT_BACKUP',
|
||||
function BackupFactory($resource, API_ENDPOINT_BACKUP) {
|
||||
'use strict';
|
||||
return $resource(
|
||||
API_ENDPOINT_BACKUP + '/:subResource/:action',
|
||||
{},
|
||||
{
|
||||
download: {
|
||||
method: 'POST',
|
||||
responseType: 'blob',
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: (data, headersGetter) => ({
|
||||
file: data,
|
||||
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
|
||||
}),
|
||||
},
|
||||
getS3Settings: { method: 'GET', params: { subResource: 's3', action: 'settings' } },
|
||||
saveS3Settings: { method: 'POST', params: { subResource: 's3', action: 'settings' } },
|
||||
exportS3Backup: { method: 'POST', params: { subResource: 's3', action: 'execute' } },
|
||||
restoreS3Backup: { method: 'POST', params: { subResource: 's3', action: 'restore' } },
|
||||
getBackupStatus: { method: 'GET', params: { subResource: 's3', action: 'status' } },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
|
@ -0,0 +1,90 @@
|
|||
angular.module('portainer.app').factory('BackupService', [
|
||||
'$q',
|
||||
'$async',
|
||||
'Backup',
|
||||
'FileUploadService',
|
||||
function BackupServiceFactory($q, $async, Backup, FileUploadService) {
|
||||
'use strict';
|
||||
const service = {};
|
||||
|
||||
service.downloadBackup = function (payload) {
|
||||
return Backup.download({}, payload).$promise;
|
||||
};
|
||||
|
||||
service.uploadBackup = function (file, password) {
|
||||
return FileUploadService.uploadBackup(file, password);
|
||||
};
|
||||
|
||||
service.getS3Settings = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Backup.getS3Settings()
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve backup S3 settings', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.saveS3Settings = function (payload) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Backup.saveS3Settings({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to save backup S3 settings', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.exportBackup = function (payload) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Backup.exportS3Backup({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to export backup', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.restoreFromS3 = function (payload) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Backup.restoreS3Backup({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to restore backup from S3', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.getBackupStatus = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Backup.getBackupStatus()
|
||||
.$promise.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve backup status', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
},
|
||||
]);
|
|
@ -61,6 +61,16 @@ angular.module('portainer.app').factory('FileUploadService', [
|
|||
});
|
||||
};
|
||||
|
||||
service.uploadBackup = function (file, password) {
|
||||
return Upload.upload({
|
||||
url: 'api/restore',
|
||||
data: {
|
||||
file,
|
||||
password,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
service.createSwarmStack = function (stackName, swarmId, file, env, endpointId) {
|
||||
return Upload.upload({
|
||||
url: 'api/stacks?method=file&type=1&endpointId=' + endpointId,
|
||||
|
|
|
@ -11,8 +11,17 @@
|
|||
<!-- init password panel -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- toggle -->
|
||||
<div style="padding-bottom: 12px;">
|
||||
<a ng-click="togglePanel()">
|
||||
<i ng-class="{ true: 'glyphicon glyphicon-chevron-down', false: 'glyphicon glyphicon-chevron-right' }[state.showInitPassword]" aria-hidden="true"></i
|
||||
><span style="padding-left: 10px;">New Portainer installation</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- !toggle -->
|
||||
|
||||
<!-- init password form -->
|
||||
<form class="simple-box-form form-horizontal">
|
||||
<form class="simple-box-form form-horizontal" style="padding-left: 30px;" ng-if="state.showInitPassword">
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
@ -98,6 +107,123 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !init password panel -->
|
||||
|
||||
<!-- restore backup panel -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- toggle -->
|
||||
<div style="padding-bottom: 12px;">
|
||||
<a ng-click="togglePanel()">
|
||||
<i ng-class="{ true: 'glyphicon glyphicon-chevron-down', false: 'glyphicon glyphicon-chevron-right' }[state.showRestorePortainer]" aria-hidden="true"></i
|
||||
><span style="padding-left: 10px;">Restore Portainer from backup</span>
|
||||
</a>
|
||||
</div>
|
||||
<!-- !toggle -->
|
||||
|
||||
<!-- restore form -->
|
||||
<form class="simple-box-form form-horizontal" style="padding-left: 30px;" ng-if="state.showRestorePortainer">
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-9">
|
||||
<span class="small text-muted">
|
||||
This will restore the Portainer metadata which contains information about the endpoints, stacks and applications, as well as the configured users.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<div class="form-group">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="restore_file" checked="checked" />
|
||||
<label for="restore_file" style="padding-bottom: 20px;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Upload backup file
|
||||
</div>
|
||||
<p></p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="restore_s3" disabled />
|
||||
<label for="restore_s3" class="boxselector_disabled" style="background-color: #ffffff;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-download" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Retrieve from S3
|
||||
</div>
|
||||
<p>This feature is available in <a href="https://www.portainer.io/business-upsell?from=restore-s3-form" target="_blank"> Portainer Business Edition</a></p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
You can upload a backup file from your computer.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<!-- select-file-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select accept=".tar.gz,.encrypted" ng-model="formValues.BackupFile" auto-focus>Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.BackupFile.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.BackupFile" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !select-file-input -->
|
||||
<!-- password-input -->
|
||||
<div class="form-group">
|
||||
<label for="password" class="col-sm-3 control-label text-left">
|
||||
Password
|
||||
<portainer-tooltip
|
||||
position="bottom"
|
||||
message="If the backup is password protected, provide the password in order to extract the backup file, otherwise this field can be left empty."
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="password" class="form-control" ng-model="formValues.Password" id="password" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- !password-input -->
|
||||
<!-- note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
You are about to restore Portainer from this backup.
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
After restoring has completed, please log in as a user that was known by the Portainer that was restored.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !note -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="!formValues.BackupFile || state.backupInProgress"
|
||||
ng-click="uploadBackup()"
|
||||
button-spinner="state.backupInProgress"
|
||||
>
|
||||
<span ng-hide="state.backupInProgress">Restore Portainer</span>
|
||||
<span ng-show="state.backupInProgress">Restoring Portainer...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
<!-- !restore backup form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restore backup panel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !simple box -->
|
||||
|
|
|
@ -8,7 +8,9 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
'SettingsService',
|
||||
'UserService',
|
||||
'EndpointService',
|
||||
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService) {
|
||||
'BackupService',
|
||||
'StatusService',
|
||||
function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, BackupService, StatusService) {
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -20,6 +22,13 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
showInitPassword: true,
|
||||
showRestorePortainer: false,
|
||||
};
|
||||
|
||||
$scope.togglePanel = function () {
|
||||
$scope.state.showInitPassword = !$scope.state.showInitPassword;
|
||||
$scope.state.showRestorePortainer = !$scope.state.showRestorePortainer;
|
||||
};
|
||||
|
||||
$scope.createAdminUser = function () {
|
||||
|
@ -67,5 +76,36 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
});
|
||||
}
|
||||
createAdministratorFlow();
|
||||
|
||||
async function waitPortainerRestart() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5 * 1000));
|
||||
try {
|
||||
const status = await StatusService.status();
|
||||
if (status && status.Version) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
throw 'Timeout to wait for Portainer restarting';
|
||||
}
|
||||
|
||||
$scope.uploadBackup = async function () {
|
||||
$scope.state.backupInProgress = true;
|
||||
|
||||
const file = $scope.formValues.BackupFile;
|
||||
const password = $scope.formValues.Password;
|
||||
|
||||
try {
|
||||
await BackupService.uploadBackup(file, password);
|
||||
await waitPortainerRestart();
|
||||
Notifications.success('The backup has successfully been restored');
|
||||
$state.go('portainer.auth');
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to restore the backup');
|
||||
} finally {
|
||||
$scope.state.backupInProgress = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -191,3 +191,87 @@
|
|||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-download" title-text="Backup Portainer"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" ng-submit="backupPortainer()" name="backupPortainerForm">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Backup configuration
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="backup_file" checked="checked" />
|
||||
<label for="backup_file" style="padding-bottom: 20px;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-download" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Download backup file
|
||||
</div>
|
||||
<p></p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="backup_s3" disabled />
|
||||
<label for="backup_s3" class="boxselector_disabled" style="background-color: #ffffff;">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Store in S3
|
||||
</div>
|
||||
<p>This feature is available in <a href="https://www.portainer.io/business-upsell?from=s3-backup-setting" target="_blank"> Portainer Business Edition</a></p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Security settings
|
||||
</div>
|
||||
<!-- Password protect -->
|
||||
<div class="form-group">
|
||||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch"> <input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i> </label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Password protect -->
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group" ng-if="formValues.passwordProtect">
|
||||
<label for="password" class="col-sm-1 control-label text-left">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" ng-model="formValues.password" id="password" name="password" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-12" ng-show="backupPortainerForm.password.$invalid">
|
||||
<div class="small text-warning">
|
||||
<div ng-messages="backupPortainerForm.password.$error">
|
||||
<p ng-message="required"> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Password -->
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="downloadBackup()"
|
||||
ng-disabled="backupPortainerForm.$invalid || state.backupInProgress"
|
||||
button-spinner="state.backupInProgress"
|
||||
>
|
||||
<span ng-hide="state.backupInProgress">Download backup</span>
|
||||
<span ng-show="state.backupInProgress">Downloading backup</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,10 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
'Notifications',
|
||||
'SettingsService',
|
||||
'StateManager',
|
||||
function ($scope, $state, Notifications, SettingsService, StateManager) {
|
||||
'BackupService',
|
||||
'FileSaver',
|
||||
'Blob',
|
||||
function ($scope, $state, Notifications, SettingsService, StateManager, BackupService, FileSaver) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
|
@ -21,6 +24,8 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
value: 30,
|
||||
},
|
||||
],
|
||||
|
||||
backupInProgress: false,
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
@ -29,6 +34,8 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
labelValue: '',
|
||||
enableEdgeComputeFeatures: false,
|
||||
enableTelemetry: false,
|
||||
passwordProtect: false,
|
||||
password: '',
|
||||
};
|
||||
|
||||
$scope.removeFilteredContainerLabel = function (index) {
|
||||
|
@ -49,6 +56,28 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
updateSettings(settings);
|
||||
};
|
||||
|
||||
$scope.downloadBackup = function () {
|
||||
const payload = {};
|
||||
if ($scope.formValues.passwordProtect) {
|
||||
payload.password = $scope.formValues.password;
|
||||
}
|
||||
|
||||
$scope.state.backupInProgress = true;
|
||||
|
||||
BackupService.downloadBackup(payload)
|
||||
.then(function success(data) {
|
||||
const downloadData = new Blob([data.file], { type: 'application/gzip' });
|
||||
FileSaver.saveAs(downloadData, data.name);
|
||||
Notifications.success('Backup successfully downloaded');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to download backup');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.backupInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveApplicationSettings = function () {
|
||||
var settings = $scope.settings;
|
||||
|
||||
|
|
Loading…
Reference in New Issue