Compare commits
58 Commits
feat/EE-19
...
fix/EE-169
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bad61e73e | ||
|
|
1cadfb1b5f | ||
|
|
bd82c5e14e | ||
|
|
bda164fca4 | ||
|
|
b090143ef1 | ||
|
|
6e200a03ba | ||
|
|
363c65a7ec | ||
|
|
235224975d | ||
|
|
cea0533d14 | ||
|
|
e066681742 | ||
|
|
a77f189b45 | ||
|
|
c83082f447 | ||
|
|
ba78168d5c | ||
|
|
5add05ad6b | ||
|
|
3225136a15 | ||
|
|
25b6b3467b | ||
|
|
cbfdd58ece | ||
|
|
5d2fe2d818 | ||
|
|
0c87634bc3 | ||
|
|
1991475437 | ||
|
|
f45edaaa76 | ||
|
|
85eca81584 | ||
|
|
1faed11fd6 | ||
|
|
042a66d15c | ||
|
|
3a066d0cd8 | ||
|
|
f3b8a9dc85 | ||
|
|
0ee42f76c3 | ||
|
|
6fe6f36696 | ||
|
|
3145b4007a | ||
|
|
93a77fd80c | ||
|
|
c809a5dbf2 | ||
|
|
8321318143 | ||
|
|
f631c1757b | ||
|
|
f540375eb7 | ||
|
|
8449f895e9 | ||
|
|
d191e4f9b9 | ||
|
|
048bd35dfb | ||
|
|
d6dbb3982a | ||
|
|
182bf10b91 | ||
|
|
740993e3a4 | ||
|
|
b94bd2e96f | ||
|
|
b2da6101b6 | ||
|
|
b43fb6b5e6 | ||
|
|
b3b168631d | ||
|
|
1cbfd96738 | ||
|
|
5e898405f5 | ||
|
|
ed500b51c6 | ||
|
|
0e60f40937 | ||
|
|
a5058e8f1e | ||
|
|
b5c59c8982 | ||
|
|
47c32df77a | ||
|
|
ee213a6c4a | ||
|
|
0979e6ec8f | ||
|
|
840d65f578 | ||
|
|
0128d1bf96 | ||
|
|
87ef8092ba | ||
|
|
6d87c77ab0 | ||
|
|
9fae031390 |
13
.babelrc
Normal file
13
.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": ["lodash", "angularjs-annotate"],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": "2"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -17,75 +17,12 @@ plugins:
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
sourceType: module
|
||||
project: './tsconfig.json'
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
|
||||
rules:
|
||||
no-control-regex: 'off'
|
||||
no-control-regex: off
|
||||
no-empty: warn
|
||||
no-empty-function: warn
|
||||
no-useless-escape: 'off'
|
||||
import/order:
|
||||
[
|
||||
'error',
|
||||
{
|
||||
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroupsExcludedImportTypes: ['internal'],
|
||||
},
|
||||
]
|
||||
|
||||
settings:
|
||||
'import/resolver':
|
||||
alias:
|
||||
map:
|
||||
- ['@', './app']
|
||||
extensions: ['.js', '.ts', '.tsx']
|
||||
|
||||
overrides:
|
||||
- files:
|
||||
- app/**/*.ts{,x}
|
||||
parserOptions:
|
||||
project: './tsconfig.json'
|
||||
parser: '@typescript-eslint/parser'
|
||||
plugins:
|
||||
- '@typescript-eslint'
|
||||
extends:
|
||||
- airbnb
|
||||
- airbnb-typescript
|
||||
- 'plugin:eslint-comments/recommended'
|
||||
- 'plugin:react-hooks/recommended'
|
||||
- 'plugin:react/jsx-runtime'
|
||||
- 'plugin:@typescript-eslint/recommended'
|
||||
- 'plugin:@typescript-eslint/eslint-recommended'
|
||||
- 'plugin:promise/recommended'
|
||||
- prettier # should be last
|
||||
settings:
|
||||
react:
|
||||
version: 'detect'
|
||||
rules:
|
||||
import/order:
|
||||
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
|
||||
func-style: [error, 'declaration']
|
||||
import/prefer-default-export: off
|
||||
no-use-before-define: ['error', { functions: false }]
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
|
||||
no-shadow: 'off'
|
||||
'@typescript-eslint/no-shadow': off
|
||||
jsx-a11y/no-autofocus: warn
|
||||
react/forbid-prop-types: off
|
||||
react/require-default-props: off
|
||||
react/no-array-index-key: off
|
||||
react/jsx-filename-extension: [0]
|
||||
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
- 'plugin:jest/recommended'
|
||||
- 'plugin:jest/style'
|
||||
env:
|
||||
'jest/globals': true
|
||||
no-useless-escape: off
|
||||
import/order: error
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -30,7 +30,7 @@ A clear and concise description of what you expected to happen.
|
||||
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
dist
|
||||
14
.prettierrc
14
.prettierrc
@@ -4,20 +4,10 @@
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.{j,t}sx"
|
||||
],
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
cssLoaderOptions: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[path][name]__[local]',
|
||||
auto: true,
|
||||
exportLocalsConvention: 'camelCaseOnly',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webpackFinal: (config) => {
|
||||
config.resolve.plugins = [
|
||||
...(config.resolve.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve.extensions,
|
||||
}),
|
||||
];
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import '../app/assets/css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
)
|
||||
|
||||
var backupDefaults = struct {
|
||||
backupDir string
|
||||
commonDir string
|
||||
databaseFileName string
|
||||
}{
|
||||
"backups",
|
||||
"common",
|
||||
databaseFileName,
|
||||
}
|
||||
|
||||
var backupLog = plog.NewScopedLog("bolt, backup")
|
||||
|
||||
//
|
||||
// Backup Helpers
|
||||
//
|
||||
|
||||
// createBackupFolders create initial folders for backups
|
||||
func (store *Store) createBackupFolders() {
|
||||
// create common dir
|
||||
commonDir := store.commonBackupDir()
|
||||
if exists, _ := store.fileService.FileExists(commonDir); !exists {
|
||||
if err := os.MkdirAll(commonDir, 0700); err != nil {
|
||||
backupLog.Error("Error while creating common backup folder", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) databasePath() string {
|
||||
return path.Join(store.path, databaseFileName)
|
||||
}
|
||||
|
||||
func (store *Store) commonBackupDir() string {
|
||||
return path.Join(store.path, backupDefaults.backupDir, backupDefaults.commonDir)
|
||||
}
|
||||
|
||||
func (store *Store) copyDBFile(from string, to string) error {
|
||||
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
|
||||
err := store.fileService.Copy(from, to, true)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BackupOptions provide a helper to inject backup options
|
||||
type BackupOptions struct {
|
||||
Version int
|
||||
BackupDir string
|
||||
BackupFileName string
|
||||
BackupPath string
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
}
|
||||
if options.Version == 0 {
|
||||
options.Version, _ = store.version()
|
||||
}
|
||||
if options.BackupDir == "" {
|
||||
options.BackupDir = store.commonBackupDir()
|
||||
}
|
||||
if options.BackupFileName == "" {
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
|
||||
}
|
||||
if options.BackupPath == "" {
|
||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// BackupWithOptions backup current database with options
|
||||
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
|
||||
backupLog.Info("creating db backup")
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
// Restore strategies:
|
||||
// - default: restore latest from current edition
|
||||
// - restore a specific
|
||||
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
|
||||
options = store.setupOptions(options)
|
||||
|
||||
// Check if backup file exist before restoring
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
backupLog.Error("Error while closing store before restore", err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info("Restoring db backup")
|
||||
err = store.copyDBFile(options.BackupPath, store.databasePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Open()
|
||||
}
|
||||
|
||||
// RemoveWithOptions removes backup database based on supplied options
|
||||
func (store *Store) RemoveWithOptions(options *BackupOptions) error {
|
||||
backupLog.Info("Removing db backup")
|
||||
|
||||
options = store.setupOptions(options)
|
||||
_, err := os.Stat(options.BackupPath)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
|
||||
return err
|
||||
}
|
||||
|
||||
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
|
||||
err = os.Remove(options.BackupPath)
|
||||
if err != nil {
|
||||
backupLog.Error("Failed", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// isFileExist is helper function to check for file existence
|
||||
func isFileExist(path string) bool {
|
||||
matches, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(matches) > 0
|
||||
}
|
||||
|
||||
func TestCreateBackupFolders(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
backupPath := path.Join(store.path, backupDefaults.backupDir)
|
||||
|
||||
if isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to not exist")
|
||||
}
|
||||
|
||||
store.createBackupFolders()
|
||||
if !isFileExist(backupPath) {
|
||||
t.Error("Expect backups folder to exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreCreation(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
if store == nil {
|
||||
t.Error("Expect to create a store")
|
||||
}
|
||||
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
t.Error("Expect to get CE Edition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackup(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("Backup should create default db backup", func(t *testing.T) {
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
store.BackupWithOptions(nil)
|
||||
|
||||
backupFileName := path.Join(store.path, "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
|
||||
store.BackupWithOptions(&BackupOptions{
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
BackupDir: store.commonBackupDir(),
|
||||
})
|
||||
backupFileName := path.Join(store.path, "backups", "common", beforePortainerVersionUpgradeBackup)
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveWithOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
t.Run("successfully removes file if existent", func(t *testing.T) {
|
||||
store.createBackupFolders()
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
filePath := path.Join(options.BackupDir, options.BackupFileName)
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("file should be created; err=%s", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
err = store.RemoveWithOptions(options)
|
||||
if err != nil {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
|
||||
}
|
||||
|
||||
if isFileExist(f.Name()) {
|
||||
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails to removes file if non-existent", func(t *testing.T) {
|
||||
options := &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: "test.txt",
|
||||
}
|
||||
|
||||
err := store.RemoveWithOptions(options)
|
||||
if err == nil {
|
||||
t.Error("RemoveWithOptions should fail for non-existent file")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package bolt
|
||||
package bolttest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -6,12 +6,13 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
var errTempDir = errors.New("can't create a temp dir")
|
||||
|
||||
func MustNewTestStore(init bool) (*Store, func()) {
|
||||
func MustNewTestStore(init bool) (*bolt.Store, func()) {
|
||||
store, teardown, err := NewTestStore(init)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errTempDir) {
|
||||
@@ -23,7 +24,7 @@ func MustNewTestStore(init bool) (*Store, func()) {
|
||||
return store, teardown
|
||||
}
|
||||
|
||||
func NewTestStore(init bool) (*Store, func(), error) {
|
||||
func NewTestStore(init bool) (*bolt.Store, func(), error) {
|
||||
// Creates unique temp directory in a concurrency friendly manner.
|
||||
dataStorePath, err := ioutil.TempDir("", "boltdb")
|
||||
if err != nil {
|
||||
@@ -35,7 +36,11 @@ func NewTestStore(init bool) (*Store, func(), error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
store := NewStore(dataStorePath, fileService)
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -55,7 +60,7 @@ func NewTestStore(init bool) (*Store, func(), error) {
|
||||
return store, teardown, nil
|
||||
}
|
||||
|
||||
func teardown(store *Store, dataStorePath string) {
|
||||
func teardown(store *bolt.Store, dataStorePath string) {
|
||||
err := store.Close()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
@@ -2,6 +2,7 @@ package bolt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/extension"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/bolt/registry"
|
||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
@@ -34,6 +36,7 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/user"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
"github.com/portainer/portainer/api/bolt/webhook"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -73,14 +76,6 @@ type Store struct {
|
||||
WebhookService *webhook.Service
|
||||
}
|
||||
|
||||
func (store *Store) version() (int, error) {
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
@@ -90,13 +85,25 @@ func (store *Store) edition() portainer.SoftwareEdition {
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string, fileService portainer.FileService) *Store {
|
||||
return &Store{
|
||||
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
|
||||
store := &Store{
|
||||
path: storePath,
|
||||
fileService: fileService,
|
||||
isNew: true,
|
||||
connection: &internal.DbConnection{},
|
||||
}
|
||||
|
||||
databasePath := path.Join(storePath, databaseFileName)
|
||||
databaseFileExists, err := fileService.FileExists(databasePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if databaseFileExists {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
@@ -108,17 +115,7 @@ func (store *Store) Open() error {
|
||||
}
|
||||
store.connection.DB = db
|
||||
|
||||
err = store.initServices()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we have DBVersion in the database then ensure we flag this as NOT a new store
|
||||
if _, err := store.VersionService.DBVersion(); err == nil {
|
||||
store.isNew = false
|
||||
}
|
||||
|
||||
return nil
|
||||
return store.initServices()
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
@@ -136,6 +133,64 @@ func (store *Store) IsNew() bool {
|
||||
return store.isNew
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
version = 0
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
log.Printf("An error occurred during database migration: %s\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (store *Store) BackupTo(w io.Writer) error {
|
||||
@@ -144,11 +199,3 @@ func (store *Store) BackupTo(w io.Writer) error {
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
return errors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ func (store *Store) Init() error {
|
||||
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
KubectlShellImage: portainer.DefaultKubectlShellImage,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
plog "github.com/portainer/portainer/api/bolt/log"
|
||||
"github.com/portainer/portainer/api/bolt/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||
|
||||
var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
store.Rollback(true)
|
||||
err = fmt.Errorf("%v", e)
|
||||
}
|
||||
}()
|
||||
|
||||
// !Important: we must use a named return value in the function definition and not a local
|
||||
// !variable referenced from the closure or else the return value will be incorrectly set
|
||||
return migrator.Migrate()
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) MigrateData(force bool) error {
|
||||
if store.isNew && !force {
|
||||
return store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
migrator, err := store.newMigrator()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// backup db file before upgrading DB to support rollback
|
||||
isUpdating, err := store.VersionService.IsUpdating()
|
||||
if err != nil && err != errors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isUpdating && migrator.Version() != portainer.DBVersion {
|
||||
err = store.backupVersion(migrator)
|
||||
if err != nil {
|
||||
return werrors.Wrapf(err, "failed to backup database")
|
||||
}
|
||||
}
|
||||
|
||||
if migrator.Version() < portainer.DBVersion {
|
||||
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
|
||||
err = store.FailSafeMigrate(migrator)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database migration", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) newMigrator() (*migrator.Migrator, error) {
|
||||
version, err := store.version()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.connection.DB,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
}
|
||||
return migrator.NewMigrator(migratorParams), nil
|
||||
}
|
||||
|
||||
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
|
||||
// - db backup prior to version upgrade
|
||||
// - db rollback
|
||||
func getBackupRestoreOptions(store *Store) *BackupOptions {
|
||||
return &BackupOptions{
|
||||
BackupDir: store.commonBackupDir(),
|
||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
||||
}
|
||||
}
|
||||
|
||||
// backupVersion will backup the database or panic if any errors occur
|
||||
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
|
||||
migrateLog.Info("Backing up database prior to version upgrade...")
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
_, err := store.BackupWithOptions(options)
|
||||
if err != nil {
|
||||
migrateLog.Error("An error occurred during database backup", err)
|
||||
removalErr := store.RemoveWithOptions(options)
|
||||
if removalErr != nil {
|
||||
migrateLog.Error("An error occurred during store removal prior to backup", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
|
||||
if !force {
|
||||
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
|
||||
if err != nil || !confirmed {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
err := store.RestoreWithOptions(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.Close()
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
if v, _ := store.version(); v != versionWant {
|
||||
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
if !store.IsNew() {
|
||||
t.Error("Expect a new DB")
|
||||
}
|
||||
|
||||
store.MigrateData(false)
|
||||
|
||||
testVersion(store, portainer.DBVersion, t)
|
||||
store.Close()
|
||||
|
||||
store.Open()
|
||||
if store.IsNew() {
|
||||
t.Error("Expect store to NOT be new DB")
|
||||
}
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
version int
|
||||
expectedVersion int
|
||||
}{
|
||||
{version: 2, expectedVersion: portainer.DBVersion},
|
||||
{version: 21, expectedVersion: portainer.DBVersion},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
store, teardown := MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
// Setup data
|
||||
store.VersionService.StoreDBVersion(tc.version)
|
||||
|
||||
// Required roles by migrations 22.2
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 1})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 2})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 3})
|
||||
store.RoleService.CreateRole(&portainer.Role{ID: 4})
|
||||
|
||||
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.MigrateData(true)
|
||||
testVersion(store, tc.expectedVersion, t)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.Rollback(true)
|
||||
store.Open()
|
||||
testVersion(store, tc.version, t)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
version := 2
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(0)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if !isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.VersionService.StoreIsUpdating(true)
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
store.MigrateData(true)
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
|
||||
options := getBackupRestoreOptions(store)
|
||||
|
||||
wantDir := store.commonBackupDir()
|
||||
if !strings.HasSuffix(options.BackupDir, wantDir) {
|
||||
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
|
||||
}
|
||||
|
||||
wantFilename := "portainer.db.bak"
|
||||
if options.BackupFileName != wantFilename {
|
||||
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := 21
|
||||
store, teardown := MustNewTestStore(false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
_, err := store.BackupWithOptions(getBackupRestoreOptions(store))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Change the current edition
|
||||
err = store.VersionService.StoreDBVersion(version + 10)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Rollback(true)
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
store.Open()
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return werrors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// set DB to updating status
|
||||
err := m.versionService.StoreIsUpdating(true)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreIsUpdating")
|
||||
}
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateAdminUserToDBVersion1")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion2")
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion2")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion3")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion4")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion5")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion6")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion7")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion8")
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion9")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion10")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion11")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToVersion12")
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToVersion12")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToVersion13")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion14")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion15")
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTemplatesToVersion15")
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion16")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateExtensionsToDBVersion17")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointGroupsToDBVersion18")
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateRegistriesToDBVersion18")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion19")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDBVersion20")
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSchedulesToDBVersion20")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateResourceControlsToDBVersion22")
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateUsersAndRolesToDBVersion22")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateTagsToDBVersion23")
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateSettingsToDB25")
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStacksToDB24")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateEndpointSettingsToDB25")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return migrationError(err, "updateStackResourceControlToDB27")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB30")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB32")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.1
|
||||
if m.currentDBVersion < 33 {
|
||||
err := m.migrateDBVersionToDB33()
|
||||
if err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB33")
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.10
|
||||
if m.currentDBVersion < 34 {
|
||||
if err := m.migrateDBVersionToDB34(); err != nil {
|
||||
return migrationError(err, "migrateDBVersionToDB34")
|
||||
}
|
||||
}
|
||||
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
|
||||
|
||||
// reset DB updating status
|
||||
return m.versionService.StoreIsUpdating(false)
|
||||
}
|
||||
@@ -176,8 +176,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
|
||||
|
||||
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [bolt,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
|
||||
continue
|
||||
return fmt.Errorf("failed fetching environment docker id: %w", err)
|
||||
}
|
||||
|
||||
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
|
||||
@@ -214,16 +213,8 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
|
||||
volumes := volumesData["Volumes"].([]interface{})
|
||||
for _, volumeMeta := range volumes {
|
||||
volume := volumeMeta.(map[string]interface{})
|
||||
volumeName, nameExist := volume["Name"].(string)
|
||||
if !nameExist {
|
||||
continue
|
||||
}
|
||||
createTime, createTimeExist := volume["CreatedAt"].(string)
|
||||
if !createTimeExist {
|
||||
continue
|
||||
}
|
||||
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
|
||||
volumeName := volume["Name"].(string)
|
||||
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
|
||||
resourceControl, ok := volumeResourceControls[oldResourceID]
|
||||
|
||||
if ok {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB33() error {
|
||||
if err := m.migrateSettingsToDB33(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) migrateSettingsToDB33() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
|
||||
return m.settingsService.UpdateSettings(settings)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
func (m *Migrator) migrateDBVersionTo33() error {
|
||||
err := migrateStackEntryPoint(m.stackService)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMigrateStackEntryPoint(t *testing.T) {
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-34.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
assert.NoError(t, err, "failed to init testing DB connection")
|
||||
defer dbConn.Close()
|
||||
|
||||
|
||||
@@ -27,9 +27,8 @@ var migrateLog = plog.NewScopedLog("bolt, migrate")
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
db *bolt.DB
|
||||
currentDBVersion int
|
||||
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
@@ -98,7 +97,295 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
}
|
||||
}
|
||||
|
||||
// Version exposes version of database
|
||||
func (migrator *Migrator) Version() int {
|
||||
return migrator.currentDBVersion
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// Portainer < 1.12
|
||||
if m.currentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.currentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.currentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.currentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.currentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.currentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1449
|
||||
if m.currentDBVersion < 7 {
|
||||
err := m.updateSettingsToVersion7()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 8 {
|
||||
err := m.updateEndpointsToVersion8()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https: //github.com/portainer/portainer/issues/1396
|
||||
if m.currentDBVersion < 9 {
|
||||
err := m.updateEndpointsToVersion9()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/461
|
||||
if m.currentDBVersion < 10 {
|
||||
err := m.updateEndpointsToVersion10()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1906
|
||||
if m.currentDBVersion < 11 {
|
||||
err := m.updateEndpointsToVersion11()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.18.0
|
||||
if m.currentDBVersion < 12 {
|
||||
err := m.updateEndpointsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToVersion12()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.0
|
||||
if m.currentDBVersion < 13 {
|
||||
err := m.updateSettingsToVersion13()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.19.2
|
||||
if m.currentDBVersion < 14 {
|
||||
err := m.updateResourceControlsToDBVersion14()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.21.0
|
||||
if m.currentDBVersion < 18 {
|
||||
err := m.updateUsersToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointGroupsToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateRegistriesToDBVersion18()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.2.0
|
||||
if m.currentDBVersion < 27 {
|
||||
err := m.updateStackResourceControlToDB27()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.6.0
|
||||
if m.currentDBVersion < 30 {
|
||||
err := m.migrateDBVersionToDB30()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.9.0
|
||||
if m.currentDBVersion < 32 {
|
||||
err := m.migrateDBVersionToDB32()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 33 {
|
||||
if err := m.migrateDBVersionTo33(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -192,8 +192,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
|
||||
var stack portainer.Stack
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
stack := portainer.Stack{}
|
||||
err := internal.UnmarshalObject(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,12 +4,18 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/bolttest"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
func newGuidString(t *testing.T) string {
|
||||
@@ -29,7 +35,7 @@ func TestService_StackByWebhookID(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
b := stackBuilder{t: t, store: store}
|
||||
@@ -87,7 +93,7 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
|
||||
}
|
||||
store, teardown := bolt.MustNewTestStore(true)
|
||||
store, teardown := bolttest.MustNewTestStore(true)
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
|
||||
@@ -15,7 +15,6 @@ const (
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
updatingKey = "DB_UPDATING"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -84,21 +83,6 @@ func (service *Service) StoreDBVersion(version int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// IsUpdating retrieves the database updating status.
|
||||
func (service *Service) IsUpdating() (bool, error) {
|
||||
isUpdating, err := service.getKey(updatingKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return strconv.ParseBool(string(isUpdating))
|
||||
}
|
||||
|
||||
// StoreIsUpdating store the database updating status.
|
||||
func (service *Service) StoreIsUpdating(isUpdating bool) error {
|
||||
return service.setKey(updatingKey, strconv.FormatBool(isUpdating))
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
var data []byte
|
||||
|
||||
@@ -3,7 +3,6 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -33,7 +32,6 @@ type Service struct {
|
||||
snapshotService portainer.SnapshotService
|
||||
chiselServer *chserver.Server
|
||||
shutdownCtx context.Context
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
@@ -217,13 +215,18 @@ func (service *Service) checkTunnels() {
|
||||
}
|
||||
}
|
||||
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
if len(tunnel.Jobs) > 0 {
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
} else {
|
||||
service.tunnelDetailsMap.Remove(item.Key)
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,6 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
// update the LastActivity
|
||||
service.SetTunnelStatusToActive(endpoint.ID)
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
@@ -80,24 +74,15 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
|
||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
waitForAgentToConnect := 2 * time.Duration(endpoint.EdgeCheckinInterval)
|
||||
|
||||
for waitForAgentToConnect >= 0 {
|
||||
waitForAgentToConnect--
|
||||
time.Sleep(time.Second)
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
break
|
||||
}
|
||||
}
|
||||
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
|
||||
tunnel = service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||
// It sets the status to ACTIVE.
|
||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
@@ -127,8 +112,6 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
|
||||
@@ -47,7 +47,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Confirm starts a rollback db cli application
|
||||
func Confirm(message string) (bool, error) {
|
||||
log.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
@@ -56,22 +56,15 @@ func initFileService(dataStorePath string) portainer.FileService {
|
||||
return fileService
|
||||
}
|
||||
|
||||
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store := bolt.NewStore(dataStorePath, fileService)
|
||||
err := store.Open()
|
||||
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
|
||||
store, err := bolt.NewStore(dataStorePath, fileService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
log.Fatalf("failed creating data store: %v", err)
|
||||
}
|
||||
|
||||
if rollback {
|
||||
err := store.Rollback(false)
|
||||
if err != nil {
|
||||
log.Fatalf("failed rolling back: %s", err)
|
||||
}
|
||||
|
||||
log.Println("Exiting rollback")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatalf("failed opening store: %v", err)
|
||||
}
|
||||
|
||||
err = store.Init()
|
||||
@@ -406,7 +399,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
|
||||
dataStore := initDataStore(*flags.Data, *flags.Rollback, fileService, shutdownCtx)
|
||||
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
|
||||
|
||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -467,8 +460,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
reverseTunnelService.ProxyManager = proxyManager
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
@@ -506,7 +497,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed getting admin password file: %v", err)
|
||||
}
|
||||
|
||||
@@ -91,11 +91,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
tunnel, err := reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package exec
|
||||
|
||||
import "regexp"
|
||||
|
||||
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -15,7 +17,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
// ComposeStackManager is a wrapper for docker-compose binary
|
||||
@@ -58,7 +59,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
filePaths := getStackFiles(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
@@ -73,14 +74,15 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
||||
defer proxy.Close()
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
filePaths := getStackFiles(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
// NormalizeStackName returns a new stack name with unsupported characters replaced
|
||||
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
@@ -115,3 +117,27 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
// getStackFiles returns list of stack's confile file paths.
|
||||
// items in the list would be sanitized according to following criterias:
|
||||
// 1. no empty paths
|
||||
// 2. no "../xxx" paths that are trying to escape stack folder
|
||||
// 3. no dir paths
|
||||
// 4. root paths would be made relative
|
||||
func getStackFiles(stack *portainer.Stack) []string {
|
||||
paths := make([]string, 0, len(stack.AdditionalFiles)+1)
|
||||
|
||||
for _, p := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
|
||||
if strings.HasPrefix(p, "/") {
|
||||
p = `.` + p
|
||||
}
|
||||
|
||||
if p == `` || p == `.` || strings.HasPrefix(p, `..`) || strings.HasSuffix(p, string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -64,3 +64,21 @@ func Test_createEnvFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getStackFiles(t *testing.T) {
|
||||
stack := &portainer.Stack{
|
||||
EntryPoint: "./file", // picks entry point
|
||||
AdditionalFiles: []string{
|
||||
``, // ignores empty string
|
||||
`.`, // ignores .
|
||||
`..`, // ignores ..
|
||||
`./dir/`, // ignrores paths that end with trailing /
|
||||
`/with-root-prefix`, // replaces "root" based paths with relative
|
||||
`./relative`, // keeps relative paths
|
||||
`../escape`, // prevents dir escape
|
||||
},
|
||||
}
|
||||
|
||||
filePaths := getStackFiles(stack)
|
||||
assert.ElementsMatch(t, filePaths, []string{`./file`, `./with-root-prefix`, `./relative`})
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package exectest
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type kubernetesMockDeployer struct{}
|
||||
|
||||
func NewKubernetesDeployer() portainer.KubernetesDeployer {
|
||||
return &kubernetesMockDeployer{}
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
@@ -95,9 +96,9 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := []string{"--token", token}
|
||||
if namespace != "" {
|
||||
args = append(args, "--namespace", namespace)
|
||||
args := []string{
|
||||
"--token", token,
|
||||
"--namespace", namespace,
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -44,26 +45,19 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
|
||||
}
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -71,10 +65,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
|
||||
@@ -94,10 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -121,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
@@ -134,10 +122,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
@@ -157,7 +142,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
|
||||
}
|
||||
}
|
||||
|
||||
return command, args, nil
|
||||
return command, args
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
|
||||
@@ -191,7 +176,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
|
||||
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
|
||||
var config map[string]interface{}
|
||||
|
||||
raw, err := manager.fileService.GetFileContent(path, "")
|
||||
raw, err := manager.fileService.GetFileContent(path)
|
||||
if err != nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
@@ -205,7 +190,8 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
|
||||
}
|
||||
|
||||
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
|
||||
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
func configureFilePaths(args []string, filePaths []string) []string {
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -69,31 +69,12 @@ type Service struct {
|
||||
fileStorePath string
|
||||
}
|
||||
|
||||
// JoinPaths takes a trusted root path and a list of untrusted paths and joins
|
||||
// them together using directory separators while enforcing that the untrusted
|
||||
// paths cannot go higher up than the trusted root
|
||||
func JoinPaths(trustedRoot string, untrustedPaths ...string) string {
|
||||
if trustedRoot == "" {
|
||||
trustedRoot = "."
|
||||
}
|
||||
|
||||
p := filepath.Join(trustedRoot, filepath.Join(append([]string{"/"}, untrustedPaths...)...))
|
||||
|
||||
// avoid setting a volume name from the untrusted paths
|
||||
vnp := filepath.VolumeName(p)
|
||||
if filepath.VolumeName(trustedRoot) == "" && vnp != "" {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(p, vnp), `\`)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// NewService initializes a new service. It creates a data directory and a directory to store files
|
||||
// inside this directory if they don't exist.
|
||||
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
service := &Service{
|
||||
dataStorePath: dataStorePath,
|
||||
fileStorePath: JoinPaths(dataStorePath, fileStorePath),
|
||||
fileStorePath: path.Join(dataStorePath, fileStorePath),
|
||||
}
|
||||
|
||||
err := os.MkdirAll(dataStorePath, 0755)
|
||||
@@ -131,12 +112,12 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
|
||||
// GetBinaryFolder returns the full path to the binary store on the filesystem
|
||||
func (service *Service) GetBinaryFolder() string {
|
||||
return JoinPaths(service.fileStorePath, BinaryStorePath)
|
||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||
}
|
||||
|
||||
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
|
||||
func (service *Service) GetDockerConfigPath() string {
|
||||
return JoinPaths(service.fileStorePath, DockerConfigPath)
|
||||
return path.Join(service.fileStorePath, DockerConfigPath)
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
@@ -147,7 +128,7 @@ func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
// GetStackProjectPath returns the absolute path on the FS for a stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||
return JoinPaths(service.wrapFileStore(ComposeStorePath), stackIdentifier)
|
||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
}
|
||||
|
||||
// Copy copies the file on fromFilePath to toFilePath
|
||||
@@ -213,13 +194,13 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
|
||||
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -227,25 +208,25 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
|
||||
return JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier)
|
||||
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
|
||||
}
|
||||
|
||||
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
|
||||
stackStorePath := JoinPaths(EdgeStackStorePath, edgeStackIdentifier)
|
||||
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
|
||||
err := service.createDirectoryInStore(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := JoinPaths(stackStorePath, fileName)
|
||||
composeFilePath := path.Join(stackStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
@@ -253,20 +234,20 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(stackStorePath), nil
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// StoreRegistryManagementFileFromBytes creates a subfolder in the
|
||||
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
|
||||
extensionStorePath := JoinPaths(ExtensionRegistryManagementStorePath, folder)
|
||||
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
|
||||
err := service.createDirectoryInStore(extensionStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
file := JoinPaths(extensionStorePath, fileName)
|
||||
file := path.Join(extensionStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(file, r)
|
||||
@@ -274,13 +255,13 @@ func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(file), nil
|
||||
return path.Join(service.fileStorePath, file), nil
|
||||
}
|
||||
|
||||
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||
// It returns the path to the newly created file.
|
||||
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
|
||||
storePath := JoinPaths(TLSStorePath, folder)
|
||||
storePath := path.Join(TLSStorePath, folder)
|
||||
err := service.createDirectoryInStore(storePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -298,13 +279,13 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := JoinPaths(storePath, fileName)
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return service.wrapFileStore(tlsFilePath), nil
|
||||
return path.Join(service.fileStorePath, tlsFilePath), nil
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an environment(endpoint).
|
||||
@@ -320,13 +301,17 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
|
||||
default:
|
||||
return "", ErrUndefinedTLSFileType
|
||||
}
|
||||
return JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName), nil
|
||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||
storePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder)
|
||||
return os.RemoveAll(storePath)
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||
@@ -343,19 +328,20 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
|
||||
return ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
filePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName)
|
||||
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||
|
||||
return os.Remove(filePath)
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileContent returns the content of a file as bytes.
|
||||
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
|
||||
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
|
||||
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if filePath == "" {
|
||||
filePath = trustedRoot
|
||||
}
|
||||
return nil, fmt.Errorf("could not get the contents of the file '%s'", filePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
@@ -373,7 +359,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, jsonContent, 0644)
|
||||
return ioutil.WriteFile(path, jsonContent, 0644)
|
||||
}
|
||||
|
||||
// FileExists checks for the existence of the specified file.
|
||||
@@ -383,17 +369,23 @@ func (service *Service) FileExists(filePath string) (bool, error) {
|
||||
|
||||
// KeyPairFilesExist checks for the existence of the key files.
|
||||
func (service *Service) KeyPairFilesExist() (bool, error) {
|
||||
privateKeyPath := JoinPaths(service.dataStorePath, PrivateKeyFile)
|
||||
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
|
||||
exists, err := service.FileExists(privateKeyPath)
|
||||
if err != nil || !exists {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
publicKeyPath := JoinPaths(service.dataStorePath, PublicKeyFile)
|
||||
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
|
||||
exists, err = service.FileExists(publicKeyPath)
|
||||
if err != nil || !exists {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -405,7 +397,12 @@ func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, p
|
||||
return err
|
||||
}
|
||||
|
||||
return service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadKeyPair retrieve the content of both key files on disk.
|
||||
@@ -425,13 +422,13 @@ func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
|
||||
|
||||
// createDirectoryInStore creates a new directory in the file store
|
||||
func (service *Service) createDirectoryInStore(name string) error {
|
||||
path := service.wrapFileStore(name)
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
return os.MkdirAll(path, 0700)
|
||||
}
|
||||
|
||||
// createFile creates a new file in the file store with the content from r.
|
||||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := service.wrapFileStore(filePath)
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
@@ -440,11 +437,15 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
|
||||
path := service.wrapFileStore(filePath)
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
block := &pem.Block{Type: fileType, Bytes: content}
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
@@ -453,13 +454,18 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
return pem.Encode(out, block)
|
||||
err = pem.Encode(out, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
path := service.wrapFileStore(filePath)
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -471,19 +477,19 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based
|
||||
// on its identifier.
|
||||
func (service *Service) GetCustomTemplateProjectPath(identifier string) string {
|
||||
return JoinPaths(service.wrapFileStore(CustomTemplateStorePath), identifier)
|
||||
return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier)
|
||||
}
|
||||
|
||||
// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) {
|
||||
customTemplateStorePath := JoinPaths(CustomTemplateStorePath, identifier)
|
||||
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
|
||||
err := service.createDirectoryInStore(customTemplateStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templateFilePath := JoinPaths(customTemplateStorePath, fileName)
|
||||
templateFilePath := path.Join(customTemplateStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(templateFilePath, r)
|
||||
@@ -491,32 +497,32 @@ func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName st
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(customTemplateStorePath), nil
|
||||
return path.Join(service.fileStorePath, customTemplateStorePath), nil
|
||||
}
|
||||
|
||||
// GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based
|
||||
// on its identifier.
|
||||
func (service *Service) GetEdgeJobFolder(identifier string) string {
|
||||
return JoinPaths(service.wrapFileStore(EdgeJobStorePath), identifier)
|
||||
return path.Join(service.fileStorePath, EdgeJobStorePath, identifier)
|
||||
}
|
||||
|
||||
// StoreEdgeJobFileFromBytes creates a subfolder in the EdgeJobStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreEdgeJobFileFromBytes(identifier string, data []byte) (string, error) {
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, identifier)
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := JoinPaths(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(filePath), nil
|
||||
return path.Join(service.fileStorePath, filePath), nil
|
||||
}
|
||||
|
||||
func createEdgeJobFileName(identifier string) string {
|
||||
@@ -526,14 +532,20 @@ func createEdgeJobFileName(identifier string) string {
|
||||
// ClearEdgeJobTaskLogs clears the Edge job task logs
|
||||
func (service *Service) ClearEdgeJobTaskLogs(edgeJobID string, taskID string) error {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
return os.Remove(path)
|
||||
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
|
||||
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
|
||||
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
fileContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -543,15 +555,20 @@ func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID st
|
||||
|
||||
// StoreEdgeJobTaskLogFileFromBytes stores the log file
|
||||
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
|
||||
edgeJobStorePath := JoinPaths(EdgeJobStorePath, edgeJobID)
|
||||
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
|
||||
err := service.createDirectoryInStore(edgeJobStorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
|
||||
r := bytes.NewReader(data)
|
||||
return service.createFileInStore(filePath, r)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
|
||||
@@ -565,7 +582,7 @@ func (service *Service) GetTemporaryPath() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return JoinPaths(service.wrapFileStore(TempPath), uid.String()), nil
|
||||
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
|
||||
}
|
||||
|
||||
// GetDataStorePath returns path to data folder
|
||||
@@ -574,12 +591,12 @@ func (service *Service) GetDatastorePath() string {
|
||||
}
|
||||
|
||||
func (service *Service) wrapFileStore(filepath string) string {
|
||||
return JoinPaths(service.fileStorePath, filepath)
|
||||
return path.Join(service.fileStorePath, filepath)
|
||||
}
|
||||
|
||||
func defaultCertPathUnderFileStore() (string, string) {
|
||||
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
|
||||
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
|
||||
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
|
||||
return certPath, keyPath
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", "d/e/f"},
|
||||
{"", "./d/e/f", "d/e/f"},
|
||||
{"", "../d/e/f", "d/e/f"},
|
||||
{"", "/d/e/f", "d/e/f"},
|
||||
{"", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", "d/e/f"},
|
||||
{".", "./d/e/f", "d/e/f"},
|
||||
{".", "../d/e/f", "d/e/f"},
|
||||
{".", "/d/e/f", "d/e/f"},
|
||||
{".", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", "d/e/f"},
|
||||
{"./", "./d/e/f", "d/e/f"},
|
||||
{"./", "../d/e/f", "d/e/f"},
|
||||
{"./", "/d/e/f", "d/e/f"},
|
||||
{"./", "../../../etc/shadow", "etc/shadow"},
|
||||
|
||||
{"a/b/c", "", "a/b/c"},
|
||||
{"a/b/c", ".", "a/b/c"},
|
||||
{"a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"/a/b/c", "", "/a/b/c"},
|
||||
{"/a/b/c", ".", "/a/b/c"},
|
||||
{"/a/b/c", "d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "./d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../d/e/f", "/a/b/c/d/e/f"},
|
||||
{"/a/b/c", "../../../etc/shadow", "/a/b/c/etc/shadow"},
|
||||
|
||||
{"./a/b/c", "", "a/b/c"},
|
||||
{"./a/b/c", ".", "a/b/c"},
|
||||
{"./a/b/c", "d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "./d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../d/e/f", "a/b/c/d/e/f"},
|
||||
{"./a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
|
||||
|
||||
{"../a/b/c", "", "../a/b/c"},
|
||||
{"../a/b/c", ".", "../a/b/c"},
|
||||
{"../a/b/c", "d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "./d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../d/e/f", "../a/b/c/d/e/f"},
|
||||
{"../a/b/c", "../../../etc/shadow", "../a/b/c/etc/shadow"},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package filesystem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinPaths(t *testing.T) {
|
||||
var ts = []struct {
|
||||
trusted string
|
||||
untrusted string
|
||||
expected string
|
||||
}{
|
||||
{"", "", "."},
|
||||
{"", ".", "."},
|
||||
{"", "d/e/f", `d\e\f`},
|
||||
{"", "./d/e/f", `d\e\f`},
|
||||
{"", "../d/e/f", `d\e\f`},
|
||||
{"", "/d/e/f", `d\e\f`},
|
||||
{"", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{".", "", "."},
|
||||
{".", ".", "."},
|
||||
{".", "d/e/f", `d\e\f`},
|
||||
{".", "./d/e/f", `d\e\f`},
|
||||
{".", "../d/e/f", `d\e\f`},
|
||||
{".", "/d/e/f", `d\e\f`},
|
||||
{".", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{".", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{".", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{".", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"./", "", "."},
|
||||
{"./", ".", "."},
|
||||
{"./", "d/e/f", `d\e\f`},
|
||||
{"./", "./d/e/f", `d\e\f`},
|
||||
{"./", "../d/e/f", `d\e\f`},
|
||||
{"./", "/d/e/f", `d\e\f`},
|
||||
{"./", "../../../windows/system.ini", `windows\system.ini`},
|
||||
{"./", `C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
|
||||
{"./", `\\server\a\b\c`, `server\a\b\c`},
|
||||
{"./", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
|
||||
|
||||
{"a/b/c", "", `a\b\c`},
|
||||
{"a/b/c", ".", `a\b\c`},
|
||||
{"a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"/a/b/c", "", `\a\b\c`},
|
||||
{"/a/b/c", ".", `\a\b\c`},
|
||||
{"/a/b/c", "d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "./d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../d/e/f", `\a\b\c\d\e\f`},
|
||||
{"/a/b/c", "../../../windows/system.ini", `\a\b\c\windows\system.ini`},
|
||||
{"/a/b/c", `C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `..\..\..\..\C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
|
||||
{"/a/b/c", `\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
{"/a/b/c", `..\..\..\..\\server\a\b\c`, `\a\b\c\server\a\b\c`},
|
||||
|
||||
{"./a/b/c", "", `a\b\c`},
|
||||
{"./a/b/c", ".", `a\b\c`},
|
||||
{"./a/b/c", "d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "./d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../d/e/f", `a\b\c\d\e\f`},
|
||||
{"./a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
|
||||
{"./a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
|
||||
{"./a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
{"./a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
|
||||
|
||||
{"../a/b/c", "", `..\a\b\c`},
|
||||
{"../a/b/c", ".", `..\a\b\c`},
|
||||
{"../a/b/c", "d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "./d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../d/e/f", `..\a\b\c\d\e\f`},
|
||||
{"../a/b/c", "../../../windows/system.ini", `..\a\b\c\windows\system.ini`},
|
||||
{"../a/b/c", `C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `..\..\..\..\C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
|
||||
{"../a/b/c", `\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
{"../a/b/c", `..\..\..\..\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
|
||||
|
||||
{"C:/a/b/c", "", `C:\a\b\c`},
|
||||
{"C:/a/b/c", ".", `C:\a\b\c`},
|
||||
{"C:/a/b/c", "d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "./d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../d/e/f", `C:\a\b\c\d\e\f`},
|
||||
{"C:/a/b/c", "../../../windows/system.ini", `C:\a\b\c\windows\system.ini`},
|
||||
{"C:/a/b/c", `C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `..\..\..\..\C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
|
||||
{"C:/a/b/c", `\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
{"C:/a/b/c", `..\..\..\..\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
|
||||
|
||||
{`\\server\a\b\c`, "", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, ".", `\\server\a\b\c`},
|
||||
{`\\server\a\b\c`, "d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "./d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../d/e/f", `\\server\a\b\c\d\e\f`},
|
||||
{`\\server\a\b\c`, "../../../windows/system.ini", `\\server\a\b\c\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `..\..\..\C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
|
||||
{`\\server\a\b\c`, `\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
{`\\server\a\b\c`, `..\..\..\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
|
||||
}
|
||||
|
||||
for _, c := range ts {
|
||||
r := JoinPaths(c.trusted, c.untrusted)
|
||||
if r != c.expected {
|
||||
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
api/go.mod
43
api/go.mod
@@ -3,48 +3,57 @@ module github.com/portainer/portainer/api
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.17
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.3.0
|
||||
github.com/go-ldap/ldap/v3 v3.1.8
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.11
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhelm v0.0.0-20210913052337-365741c1c320
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.3
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
k8s.io/api v0.17.2
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
)
|
||||
|
||||
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
|
||||
|
||||
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456
|
||||
|
||||
975
api/go.sum
975
api/go.sum
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,6 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id AuthenticateUser
|
||||
// @summary Authenticate
|
||||
// @description **Access policy**: public
|
||||
// @description Use this environment(endpoint) to authenticate against Portainer using a username and password.
|
||||
// @tags auth
|
||||
// @accept json
|
||||
|
||||
@@ -44,7 +44,6 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
|
||||
|
||||
// @id ValidateOAuth
|
||||
// @summary Authenticate with OAuth
|
||||
// @description **Access policy**: public
|
||||
// @tags auth
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
// @id Logout
|
||||
// @summary Logout
|
||||
// @description **Access policy**: authenticated
|
||||
// @security jwt
|
||||
// @tags auth
|
||||
// @success 204 "Success"
|
||||
|
||||
@@ -27,9 +27,8 @@ func (p *backupPayload) Validate(r *http.Request) error {
|
||||
// @description **Access policy**: admin
|
||||
// @tags backup
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce octet-stream
|
||||
// @param body body backupPayload false "An object contains the password to encrypt the backup with"
|
||||
// @param Password body string false "Password to encrypt the backup with"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
|
||||
@@ -22,9 +22,10 @@ type restorePayload struct {
|
||||
// @description Triggers a system restore using provided backup file
|
||||
// @description **Access policy**: public
|
||||
// @tags backup
|
||||
// @accept json
|
||||
// @param restorePayload body restorePayload true "Restore request payload"
|
||||
// @success 200 "Success"
|
||||
// @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]
|
||||
|
||||
@@ -3,7 +3,6 @@ package customtemplates
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
@@ -130,20 +129,9 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
|
||||
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidNote(note string) bool {
|
||||
if govalidator.IsNull(note) {
|
||||
return true
|
||||
}
|
||||
match, _ := regexp.MatchString("<img", note)
|
||||
return !match
|
||||
}
|
||||
|
||||
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
|
||||
var payload customTemplateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
@@ -230,9 +218,6 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
|
||||
return errors.New("Invalid custom template type")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -300,9 +285,6 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
|
||||
payload.Logo = logo
|
||||
|
||||
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
|
||||
if !isValidNote(note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
payload.Note = note
|
||||
|
||||
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// @id CustomTemplateDelete
|
||||
// @summary Remove a template
|
||||
// @description Remove a template.
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @param id path int true "Template identifier"
|
||||
|
||||
@@ -2,6 +2,7 @@ package customtemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -17,7 +18,7 @@ type fileResponse struct {
|
||||
// @id CustomTemplateFile
|
||||
// @summary Get Template stack file content.
|
||||
// @description Retrieve the content of the Stack file for the specified custom template
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @produce json
|
||||
@@ -40,7 +41,7 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.EntryPoint)
|
||||
fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Template identifier"
|
||||
// @success 200 {object} portainer.CustomTemplate "Success"
|
||||
|
||||
@@ -51,9 +51,6 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Description) {
|
||||
return errors.New("Invalid custom template description")
|
||||
}
|
||||
if !isValidNote(payload.Note) {
|
||||
return errors.New("Invalid note. <img> tag is not supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeGroupCreate
|
||||
// @summary Create an EdgeGroup
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
|
||||
// @id EdgeGroupDelete
|
||||
// @summary Deletes an EdgeGroup
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 204
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
|
||||
// @id EdgeGroupInspect
|
||||
// @summary Inspects an EdgeGroup
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EdgeGroup Id"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
|
||||
@@ -17,9 +17,10 @@ type decoratedEdgeGroup struct {
|
||||
|
||||
// @id EdgeGroupList
|
||||
// @summary list EdgeGroups
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups"
|
||||
// @failure 500
|
||||
|
||||
@@ -36,7 +36,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EgeGroupUpdate
|
||||
// @summary Updates an EdgeGroup
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -16,9 +16,10 @@ import (
|
||||
|
||||
// @id EdgeJobCreate
|
||||
// @summary Create an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file, string)
|
||||
// @param body_string body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
|
||||
// @id EdgeJobDelete
|
||||
// @summary Delete an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
|
||||
@@ -16,9 +16,10 @@ type edgeJobFileResponse struct {
|
||||
|
||||
// @id EdgeJobFile
|
||||
// @summary Fetch a file of an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} edgeJobFileResponse
|
||||
@@ -39,7 +40,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
|
||||
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ type edgeJobInspectResponse struct {
|
||||
|
||||
// @id EdgeJobInspect
|
||||
// @summary Inspect an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {object} portainer.EdgeJob
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
// @id EdgeJobList
|
||||
// @summary Fetch EdgeJobs list
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeJob
|
||||
// @failure 500
|
||||
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
|
||||
// @id EdgeJobTasksClear
|
||||
// @summary Clear the log for a specifc task on an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
|
||||
// @id EdgeJobTasksCollect
|
||||
// @summary Collect the log for a specifc task on an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -15,9 +15,10 @@ type fileResponse struct {
|
||||
|
||||
// @id EdgeJobTaskLogsInspect
|
||||
// @summary Fetch the log for a specifc task on an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @param taskID path string true "Task Id"
|
||||
|
||||
@@ -19,9 +19,10 @@ type taskContainer struct {
|
||||
|
||||
// @id EdgeJobTasksList
|
||||
// @summary Fetch the list of tasks on an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeJob Id"
|
||||
// @success 200 {array} taskContainer
|
||||
|
||||
@@ -30,7 +30,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeJobUpdate
|
||||
// @summary Update an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_jobs
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -19,9 +19,10 @@ import (
|
||||
|
||||
// @id EdgeStackCreate
|
||||
// @summary Create an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file,string,repository)
|
||||
// @param body_string body swarmStackFromFileContentPayload true "Required when using method=string"
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
|
||||
// @id EdgeStackDelete
|
||||
// @summary Delete an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 204
|
||||
// @failure 500
|
||||
|
||||
@@ -2,6 +2,7 @@ package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -16,9 +17,10 @@ type stackFileResponse struct {
|
||||
|
||||
// @id EdgeStackFile
|
||||
// @summary Fetches the stack file for an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} stackFileResponse
|
||||
@@ -44,7 +46,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
|
||||
fileName = stack.ManifestPath
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import (
|
||||
|
||||
// @id EdgeStackInspect
|
||||
// @summary Inspect an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path string true "EdgeStack Id"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
// @id EdgeStackList
|
||||
// @summary Fetches the list of EdgeStacks
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.EdgeStack
|
||||
// @failure 500
|
||||
|
||||
@@ -33,7 +33,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
|
||||
// @id EdgeStackUpdate
|
||||
// @summary Update an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_stacks
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -3,6 +3,7 @@ package edgestacks
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -55,7 +56,7 @@ func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer
|
||||
return nil
|
||||
}
|
||||
|
||||
composeConfig, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, edgeStack.EntryPoint)
|
||||
composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve Compose file from disk: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type templateFileFormat struct {
|
||||
|
||||
// @id EdgeTemplateList
|
||||
// @summary Fetches the list of Edge Templates
|
||||
// @description **Access policy**: administrator
|
||||
// @description
|
||||
// @tags edge_templates
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -21,7 +21,7 @@ func (payload *logsPayload) Validate(r *http.Request) error {
|
||||
|
||||
// endpointEdgeJobsLogs
|
||||
// @summary Inspect an EdgeJob Log
|
||||
// @description **Access policy**: public
|
||||
// @description
|
||||
// @tags edge, endpoints
|
||||
// @accept json
|
||||
// @produce json
|
||||
|
||||
@@ -3,6 +3,7 @@ package endpointedge
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -18,7 +19,7 @@ type configResponse struct {
|
||||
}
|
||||
|
||||
// @summary Inspect an Edge Stack for an Environment(Endpoint)
|
||||
// @description **Access policy**: public
|
||||
// @description
|
||||
// @tags edge, endpoints, edge_stacks
|
||||
// @accept json
|
||||
// @produce json
|
||||
@@ -74,7 +75,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
|
||||
@@ -2,12 +2,14 @@ package endpointproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -35,9 +37,22 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -35,9 +37,22 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/association [put]
|
||||
// @router /api/endpoints/{id}/association [put]
|
||||
func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,27 +18,11 @@ import (
|
||||
)
|
||||
|
||||
type dockerhubStatusResponse struct {
|
||||
// Remaiming images to pull
|
||||
Remaining int `json:"remaining"`
|
||||
// Daily limit
|
||||
Limit int `json:"limit"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// @id endpointDockerhubStatus
|
||||
// @summary fetch docker pull limits
|
||||
// @description get docker pull limits for a docker hub registry in the environment
|
||||
// @description **Access policy**:
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "endpoint ID"
|
||||
// @param registryId path int true "registry ID"
|
||||
// @success 200 {object} dockerhubStatusResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "registry or endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/dockerhub/{registryId} [get]
|
||||
// GET request on /api/endpoints/{id}/dockerhub/{registryId}
|
||||
func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -29,12 +29,6 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id endpointExtensionAdd
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions [post]
|
||||
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,13 +12,6 @@ import (
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// @id endpointExtensionRemove
|
||||
// @tags endpoints
|
||||
// @deprecated
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param extensionType path string true "Extension Type"
|
||||
// @success 204 "Success"
|
||||
// @router /endpoints/{id}/extensions/{extensionType} [delete]
|
||||
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,20 +12,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// @id endpointRegistryInspect
|
||||
// @summary get registry for environment
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "identifier"
|
||||
// @param registryId path int true "Registry identifier"
|
||||
// @success 200 {object} portainer.Registry "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Registry not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries/{registryId} [get]
|
||||
// GET request on /endpoints/{id}/registries/{registryId}
|
||||
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -13,18 +13,7 @@ import (
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
// @id endpointRegistriesList
|
||||
// @summary List Registries on environment
|
||||
// @description List all registries based on the current user authorizations in current environment.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @param namespace query string false "required if kubernetes environment, will show registries by namespace"
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @success 200 {array} portainer.Registry "Success"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries [get]
|
||||
// GET request on /endpoints/{id}/registries?namespace
|
||||
func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,22 +21,7 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id endpointRegistryAccess
|
||||
// @summary update registry access for environment
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param registryId path int true "Registry identifier"
|
||||
// @param body body registryAccessPayload true "details"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "Endpoint not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/registries/{registryId} [put]
|
||||
// PUT request on /endpoints/{id}/registries/{registryId}
|
||||
func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -36,9 +36,9 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
|
||||
// @id EndpointSettingsUpdate
|
||||
// @summary Update settings for an environment(endpoint)
|
||||
// @description Update settings for an environment(endpoint).
|
||||
// @description **Access policy**: authenticated
|
||||
// @summary Update settings for an environments(endpoints)
|
||||
// @description Update settings for an environments(endpoints).
|
||||
// @description **Access policy**: administrator
|
||||
// @security jwt
|
||||
// @tags endpoints
|
||||
// @accept json
|
||||
@@ -49,7 +49,7 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/{id}/settings [put]
|
||||
// @router /api/endpoints/{id}/settings [put]
|
||||
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
)
|
||||
|
||||
// @id EndpointSnapshot
|
||||
// @summary Snapshots an environment(endpoint)
|
||||
// @description Snapshots an environment(endpoint)
|
||||
// @description **Access policy**: administrator
|
||||
// @summary Snapshots an environments(endpoints)
|
||||
// @description Snapshots an environments(endpoints)
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
|
||||
@@ -131,7 +131,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
Version: job.Version,
|
||||
}
|
||||
|
||||
file, err := handler.FileService.GetFileContent("", job.ScriptPath)
|
||||
file, err := handler.FileService.GetFileContent(job.ScriptPath)
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||
// @id EndpointUpdate
|
||||
// @summary Update an environment(endpoint)
|
||||
// @description Update an environment(endpoint).
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: administrator
|
||||
// @security jwt
|
||||
// @tags endpoints
|
||||
// @accept json
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||
@@ -27,7 +26,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/ssl"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/storybook"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
@@ -55,7 +53,6 @@ type Handler struct {
|
||||
HelmTemplatesHandler *helm.Handler
|
||||
KubernetesHandler *kubernetes.Handler
|
||||
FileHandler *file.Handler
|
||||
LDAPHandler *ldap.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
@@ -64,7 +61,6 @@ type Handler struct {
|
||||
SSLHandler *ssl.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
@@ -76,7 +72,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.9.3
|
||||
// @version 2.9.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
@@ -193,8 +189,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/ldap"):
|
||||
http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
@@ -229,8 +223,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
|
||||
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/storybook"):
|
||||
http.StripPrefix("/storybook", h.StorybookHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/"):
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -27,17 +27,15 @@ type Handler struct {
|
||||
requestBouncer requestBouncer
|
||||
dataStore portainer.DataStore
|
||||
kubeConfigService kubernetes.KubeConfigService
|
||||
kubernetesDeployer portainer.KubernetesDeployer
|
||||
helmPackageManager libhelm.HelmPackageManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
dataStore: dataStore,
|
||||
kubernetesDeployer: kubernetesDeployer,
|
||||
helmPackageManager: helmPackageManager,
|
||||
kubeConfigService: kubeConfigService,
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import (
|
||||
// @id HelmDelete
|
||||
// @summary Delete Helm Release
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param release path string true "The name of the release/application to uninstall"
|
||||
// @param namespace query string true "An optional namespace"
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
"github.com/portainer/libhelm/binary/test"
|
||||
"github.com/portainer/libhelm/options"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
@@ -30,10 +29,9 @@ func Test_helmDelete(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "Error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes/validation"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type installChartPayload struct {
|
||||
@@ -36,7 +31,7 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
|
||||
// @id HelmInstall
|
||||
// @summary Install Helm Chart
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
@@ -136,98 +131,5 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.updateHelmAppManifest(r, manifest, installOpts.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// applyPortainerLabelsToHelmAppManifest will patch all the resources deployed in the helm release manifest
|
||||
// with portainer specific labels. This is to mark the resources as managed by portainer - hence the helm apps
|
||||
// wont appear external in the portainer UI.
|
||||
func (handler *Handler) applyPortainerLabelsToHelmAppManifest(r *http.Request, installOpts options.InstallOptions, manifest string) ([]byte, error) {
|
||||
// Patch helm release by adding with portainer labels to all deployed resources
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to retrieve user details from authentication token")
|
||||
}
|
||||
user, err := handler.dataStore.User().User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to load user information from the database")
|
||||
}
|
||||
|
||||
appLabels := kubernetes.GetHelmAppLabels(installOpts.Name, user.Username)
|
||||
labeledManifest, err := kubernetes.AddAppLabels([]byte(manifest), appLabels)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to label helm release manifest")
|
||||
}
|
||||
|
||||
return labeledManifest, nil
|
||||
}
|
||||
|
||||
// updateHelmAppManifest will update the resources of helm release manifest with portainer labels using kubectl.
|
||||
// The resources of the manifest will be updated in parallel and individuallly since resources of a chart
|
||||
// can be deployed to different namespaces.
|
||||
// NOTE: These updates will need to be re-applied when upgrading the helm release
|
||||
func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte, namespace string) error {
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to find an endpoint on request context")
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve user details from authentication token")
|
||||
}
|
||||
|
||||
// extract list of yaml resources from helm manifest
|
||||
yamlResources, err := kubernetes.ExtractDocuments(manifest, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to extract documents from helm release manifest")
|
||||
}
|
||||
|
||||
// deploy individual resources in parallel
|
||||
g := new(errgroup.Group)
|
||||
for _, resource := range yamlResources {
|
||||
resource := resource // https://golang.org/doc/faq#closures_and_goroutines
|
||||
g.Go(func() error {
|
||||
tmpfile, err := ioutil.TempFile("", "helm-manifest-*")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create a tmp helm manifest file")
|
||||
}
|
||||
defer func() {
|
||||
tmpfile.Close()
|
||||
os.Remove(tmpfile.Name())
|
||||
}()
|
||||
|
||||
if _, err := tmpfile.Write(resource); err != nil {
|
||||
return errors.Wrap(err, "failed to write a tmp helm manifest file")
|
||||
}
|
||||
|
||||
// get resource namespace, fallback to provided namespace if not explicit on resource
|
||||
resourceNamespace, err := kubernetes.GetNamespace(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resourceNamespace == "" {
|
||||
resourceNamespace = namespace
|
||||
}
|
||||
|
||||
_, err = handler.kubernetesDeployer.Deploy(tokenData.ID, endpoint, []string{tmpfile.Name()}, resourceNamespace)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != nil {
|
||||
return errors.Wrap(err, "unable to patch helm release using kubectl")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ import (
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
@@ -32,10 +31,9 @@ func Test_helmInstall(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
is.NotNil(h, "Handler should not fail")
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// @id HelmList
|
||||
// @summary List Helm Releases
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @security jwt
|
||||
// @accept json
|
||||
|
||||
@@ -11,11 +11,10 @@ import (
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/exec/exectest"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
bolt "github.com/portainer/portainer/api/bolt/bolttest"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
@@ -29,10 +28,9 @@ func Test_helmList(t *testing.T) {
|
||||
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
assert.NoError(t, err, "error creating a user")
|
||||
|
||||
kubernetesDeployer := exectest.NewKubernetesDeployer()
|
||||
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
|
||||
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
|
||||
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
|
||||
|
||||
// Install a single chart. We expect to get these values back
|
||||
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// @id HelmRepoSearch
|
||||
// @summary Search Helm Charts
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @security jwt
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
// @id HelmShow
|
||||
// @summary Show Helm Chart Information
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags helm
|
||||
// @description **Access policy**: authorized
|
||||
// @tags helm_chart
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @param chart query string true "Chart name"
|
||||
// @param command path string true "chart/values/readme"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user