Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60ca183967 | |||
| 9cefa2f1e8 | |||
| 4e38cf1f40 |
@@ -2,17 +2,18 @@ name: Bug Report
|
||||
description: Create a report to help us improve.
|
||||
labels: kind/bug,bug/need-confirmation
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Welcome!
|
||||
|
||||
|
||||
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
|
||||
|
||||
|
||||
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
|
||||
|
||||
Please note that we only provide support for current versions of Portainer. You can find a list of supported versions in our [lifecycle policy](https://docs.portainer.io/start/lifecycle).
|
||||
|
||||
|
||||
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
|
||||
|
||||
- type: checkboxes
|
||||
@@ -44,7 +45,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -70,7 +71,7 @@ body:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -91,44 +92,34 @@ body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Portainer version
|
||||
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [updating first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
|
||||
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
|
||||
multiple: false
|
||||
options:
|
||||
- '2.31.3'
|
||||
- '2.31.2'
|
||||
- '2.31.1'
|
||||
- '2.31.0'
|
||||
- '2.30.1'
|
||||
- '2.30.0'
|
||||
- '2.29.2'
|
||||
- '2.29.1'
|
||||
- '2.29.0'
|
||||
- '2.28.1'
|
||||
- '2.28.0'
|
||||
- '2.27.9'
|
||||
- '2.27.8'
|
||||
- '2.27.7'
|
||||
- '2.27.6'
|
||||
- '2.27.5'
|
||||
- '2.27.4'
|
||||
- '2.27.3'
|
||||
- '2.27.2'
|
||||
- '2.27.1'
|
||||
- '2.27.0'
|
||||
- '2.26.1'
|
||||
- '2.26.0'
|
||||
- '2.25.1'
|
||||
- '2.25.0'
|
||||
- '2.24.1'
|
||||
- '2.24.0'
|
||||
- '2.23.0'
|
||||
- '2.22.0'
|
||||
- '2.21.5'
|
||||
- '2.21.4'
|
||||
- '2.21.3'
|
||||
- '2.21.2'
|
||||
- '2.21.1'
|
||||
- '2.21.0'
|
||||
- '2.20.3'
|
||||
- '2.20.2'
|
||||
- '2.20.1'
|
||||
- '2.20.0'
|
||||
- '2.19.5'
|
||||
- '2.19.4'
|
||||
- '2.19.3'
|
||||
- '2.19.2'
|
||||
- '2.19.1'
|
||||
- '2.19.0'
|
||||
- '2.18.4'
|
||||
- '2.18.3'
|
||||
- '2.18.2'
|
||||
- '2.18.1'
|
||||
- '2.17.1'
|
||||
- '2.17.0'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -166,7 +157,7 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Browser
|
||||
description: |
|
||||
description: |
|
||||
Enter your browser and version. Example: Google Chrome 114.0
|
||||
validations:
|
||||
required: false
|
||||
|
||||
+2
-1
@@ -12,7 +12,6 @@ linters:
|
||||
- copyloopvar
|
||||
- intrange
|
||||
- perfsprint
|
||||
- ineffassign
|
||||
|
||||
linters-settings:
|
||||
depguard:
|
||||
@@ -21,6 +20,8 @@ linters-settings:
|
||||
deny:
|
||||
- pkg: 'encoding/json'
|
||||
desc: 'use github.com/segmentio/encoding/json'
|
||||
- pkg: 'github.com/sirupsen/logrus'
|
||||
desc: 'logging is allowed only by github.com/rs/zerolog'
|
||||
- pkg: 'golang.org/x/exp'
|
||||
desc: 'exp is not allowed'
|
||||
- pkg: 'github.com/portainer/libcrypto'
|
||||
|
||||
+50
-5
@@ -2,6 +2,7 @@ package archive
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -11,6 +12,50 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
||||
func UnzipArchive(archiveData []byte, dest string) error {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, zipFile := range zipReader.File {
|
||||
err := extractFileFromArchive(zipFile, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFileFromArchive(file *zip.File, dest string) error {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, file.Name)
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return outFile.Close()
|
||||
}
|
||||
|
||||
// UnzipFile will decompress a zip archive, moving all files and folders
|
||||
// within the zip file (parameter 1) to an output directory (parameter 2).
|
||||
func UnzipFile(src string, dest string) error {
|
||||
@@ -31,11 +76,11 @@ func UnzipFile(src string, dest string) error {
|
||||
if f.FileInfo().IsDir() {
|
||||
// Make Folder
|
||||
os.MkdirAll(p, os.ModePerm)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err := unzipFile(f, p); err != nil {
|
||||
err = unzipFile(f, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -48,20 +93,20 @@ func unzipFile(f *zip.File, p string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
|
||||
return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if _, err = io.Copy(outFile, rc); err != nil {
|
||||
_, err = io.Copy(outFile, rc)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
|
||||
}
|
||||
|
||||
|
||||
@@ -60,9 +60,6 @@ func CLIFlags() *portainer.CLIFlags {
|
||||
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
|
||||
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
|
||||
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
|
||||
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
|
||||
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
|
||||
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -19,5 +19,7 @@ func Confirm(message string) (bool, error) {
|
||||
}
|
||||
|
||||
answer = strings.ReplaceAll(answer, "\n", "")
|
||||
return strings.EqualFold(answer, "y") || strings.EqualFold(answer, "yes"), nil
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
}
|
||||
|
||||
+16
-17
@@ -4,21 +4,20 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultPullLimitCheckDisabled = "false"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
)
|
||||
|
||||
+17
-18
@@ -1,22 +1,21 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
defaultPullLimitCheckDisabled = "false"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultHTTPSBindAddress = ":9443"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultHTTPDisabled = "false"
|
||||
defaultHTTPEnabled = "false"
|
||||
defaultSSL = "false"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultBaseURL = "/"
|
||||
defaultSecretKeyName = "portainer"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type pairListBool []portainer.Pair
|
||||
|
||||
// Set implementation for a list of portainer.Pair
|
||||
func (l *pairListBool) Set(value string) error {
|
||||
p := new(portainer.Pair)
|
||||
|
||||
// default to true. example setting=true is equivalent to setting
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
p.Name = parts[0]
|
||||
p.Value = "true"
|
||||
} else {
|
||||
p.Name = parts[0]
|
||||
p.Value = parts[1]
|
||||
}
|
||||
|
||||
*l = append(*l, *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implementation for a list of pair
|
||||
func (l *pairListBool) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsCumulative implementation for a list of pair
|
||||
func (l *pairListBool) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func BoolPairs(s kingpin.Settings) (target *[]portainer.Pair) {
|
||||
target = new([]portainer.Pair)
|
||||
s.SetValue((*pairListBool)(target))
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package logs
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func ConfigureLogger() {
|
||||
func configureLogger() {
|
||||
zerolog.ErrorStackFieldName = "stack_trace"
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
@@ -21,7 +21,7 @@ func ConfigureLogger() {
|
||||
log.Logger = log.Logger.With().Caller().Stack().Logger()
|
||||
}
|
||||
|
||||
func SetLoggingLevel(level string) {
|
||||
func setLoggingLevel(level string) {
|
||||
switch level {
|
||||
case "ERROR":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
@@ -34,7 +34,7 @@ func SetLoggingLevel(level string) {
|
||||
}
|
||||
}
|
||||
|
||||
func SetLoggingMode(mode string) {
|
||||
func setLoggingMode(mode string) {
|
||||
switch mode {
|
||||
case "PRETTY":
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
+15
-34
@@ -39,7 +39,6 @@ import (
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/pendingactions"
|
||||
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||
@@ -50,9 +49,7 @@ import (
|
||||
"github.com/portainer/portainer/pkg/build"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/portainer/portainer/pkg/libhelm"
|
||||
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
|
||||
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -168,12 +165,12 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
|
||||
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
|
||||
}
|
||||
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
|
||||
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
|
||||
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
|
||||
}
|
||||
|
||||
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
|
||||
return libhelm.NewHelmPackageManager()
|
||||
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
|
||||
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
|
||||
}
|
||||
|
||||
func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
|
||||
@@ -241,10 +238,10 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
||||
return err
|
||||
}
|
||||
|
||||
settings.SnapshotInterval = cmp.Or(*flags.SnapshotInterval, settings.SnapshotInterval)
|
||||
settings.LogoURL = cmp.Or(*flags.Logo, settings.LogoURL)
|
||||
settings.EnableEdgeComputeFeatures = cmp.Or(*flags.EnableEdgeComputeFeatures, settings.EnableEdgeComputeFeatures)
|
||||
settings.TemplatesURL = cmp.Or(*flags.Templates, settings.TemplatesURL)
|
||||
settings.SnapshotInterval = *cmp.Or(flags.SnapshotInterval, &settings.SnapshotInterval)
|
||||
settings.LogoURL = *cmp.Or(flags.Logo, &settings.LogoURL)
|
||||
settings.EnableEdgeComputeFeatures = *cmp.Or(flags.EnableEdgeComputeFeatures, &settings.EnableEdgeComputeFeatures)
|
||||
settings.TemplatesURL = *cmp.Or(flags.Templates, &settings.TemplatesURL)
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
@@ -331,18 +328,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
|
||||
}
|
||||
|
||||
trustedOrigins := []string{}
|
||||
if *flags.TrustedOrigins != "" {
|
||||
// validate if the trusted origins are valid urls
|
||||
for _, origin := range strings.Split(*flags.TrustedOrigins, ",") {
|
||||
if !validate.IsTrustedOrigin(origin) {
|
||||
log.Fatal().Str("trusted_origin", origin).Msg("invalid url for trusted origin. Please check the trusted origins flag.")
|
||||
}
|
||||
|
||||
trustedOrigins = append(trustedOrigins, origin)
|
||||
}
|
||||
}
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
||||
if encryptionKey == nil {
|
||||
@@ -383,8 +368,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
gitService := git.NewService(shutdownCtx)
|
||||
|
||||
// Setting insecureSkipVerify to true to preserve the old behaviour.
|
||||
openAMTService := openamt.NewService(true)
|
||||
openAMTService := openamt.NewService()
|
||||
|
||||
cryptoService := &crypto.Service{}
|
||||
|
||||
@@ -437,7 +421,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||
}
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
|
||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets)
|
||||
|
||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
|
||||
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
|
||||
@@ -453,7 +437,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
|
||||
|
||||
helmPackageManager, err := initHelmPackageManager()
|
||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
||||
}
|
||||
@@ -559,7 +543,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
BindAddressHTTPS: *flags.AddrHTTPS,
|
||||
CSP: *flags.CSP,
|
||||
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
@@ -592,19 +575,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
AdminCreationDone: adminCreationDone,
|
||||
PendingActionsService: pendingActionsService,
|
||||
PlatformService: platformService,
|
||||
PullLimitCheckDisabled: *flags.PullLimitCheckDisabled,
|
||||
TrustedOrigins: trustedOrigins,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
logs.ConfigureLogger()
|
||||
logs.SetLoggingMode("PRETTY")
|
||||
configureLogger()
|
||||
setLoggingMode("PRETTY")
|
||||
|
||||
flags := initCLI()
|
||||
|
||||
logs.SetLoggingLevel(*flags.LogLevel)
|
||||
logs.SetLoggingMode(*flags.LogMode)
|
||||
setLoggingLevel(*flags.LogLevel)
|
||||
setLoggingMode(*flags.LogMode)
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
|
||||
type ReadTransaction interface {
|
||||
GetObject(bucketName string, key []byte, object any) error
|
||||
GetRawBytes(bucketName string, key []byte) ([]byte, error)
|
||||
GetAll(bucketName string, obj any, append func(o any) (any, error)) error
|
||||
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error
|
||||
KeyExists(bucketName string, key []byte) (bool, error)
|
||||
}
|
||||
|
||||
type Transaction interface {
|
||||
|
||||
@@ -138,8 +138,6 @@ func (connection *DbConnection) Open() error {
|
||||
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
InitialMmapSize: connection.InitialMmapSize,
|
||||
FreelistType: bolt.FreelistMapType,
|
||||
NoFreelistSync: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -246,32 +244,6 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetRawBytes(bucketName string, key []byte) ([]byte, error) {
|
||||
var value []byte
|
||||
|
||||
err := connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
value, err = tx.GetRawBytes(bucketName, key)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return value, err
|
||||
}
|
||||
|
||||
func (connection *DbConnection) KeyExists(bucketName string, key []byte) (bool, error) {
|
||||
var exists bool
|
||||
|
||||
err := connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
exists, err = tx.KeyExists(bucketName, key)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (connection *DbConnection) getEncryptionKey() []byte {
|
||||
if !connection.isEncrypted {
|
||||
return nil
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
@@ -32,33 +31,6 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) er
|
||||
return tx.conn.UnmarshalObject(value, object)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetRawBytes(bucketName string, key []byte) ([]byte, error) {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
value := bucket.Get(key)
|
||||
if value == nil {
|
||||
return nil, fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
if tx.conn.getEncryptionKey() != nil {
|
||||
var err error
|
||||
|
||||
if value, err = decrypt(value, tx.conn.getEncryptionKey()); err != nil {
|
||||
return value, errors.Wrap(err, "Failed decrypting object")
|
||||
}
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) KeyExists(bucketName string, key []byte) (bool, error) {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
value := bucket.Get(key)
|
||||
|
||||
return value != nil, nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error {
|
||||
data, err := tx.conn.MarshalObject(object)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
type BaseCRUD[T any, I constraints.Integer] interface {
|
||||
Create(element *T) error
|
||||
Read(ID I) (*T, error)
|
||||
Exists(ID I) (bool, error)
|
||||
ReadAll(predicates ...func(T) bool) ([]T, error)
|
||||
ReadAll() ([]T, error)
|
||||
Update(ID I, element *T) error
|
||||
Delete(ID I) error
|
||||
}
|
||||
@@ -43,26 +42,12 @@ func (service BaseDataService[T, I]) Read(ID I) (*T, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (service BaseDataService[T, I]) Exists(ID I) (bool, error) {
|
||||
var exists bool
|
||||
|
||||
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
exists, err = service.Tx(tx).Exists(ID)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
// ReadAll retrieves all the elements that satisfy all the provided predicates.
|
||||
func (service BaseDataService[T, I]) ReadAll(predicates ...func(T) bool) ([]T, error) {
|
||||
func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
|
||||
var collection = make([]T, 0)
|
||||
|
||||
return collection, service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
collection, err = service.Tx(tx).ReadAll(predicates...)
|
||||
collection, err = service.Tx(tx).ReadAll()
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package dataservices
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testObject struct {
|
||||
ID int
|
||||
Value int
|
||||
}
|
||||
|
||||
type mockConnection struct {
|
||||
store map[int]testObject
|
||||
|
||||
portainer.Connection
|
||||
}
|
||||
|
||||
func (m mockConnection) UpdateObject(bucket string, key []byte, value interface{}) error {
|
||||
obj := value.(*testObject)
|
||||
|
||||
m.store[obj.ID] = *obj
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockConnection) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
|
||||
for _, v := range m.store {
|
||||
if _, err := appendFn(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockConnection) UpdateTx(fn func(portainer.Transaction) error) error {
|
||||
return fn(m)
|
||||
}
|
||||
|
||||
func (m mockConnection) ViewTx(fn func(portainer.Transaction) error) error {
|
||||
return fn(m)
|
||||
}
|
||||
|
||||
func (m mockConnection) ConvertToKey(v int) []byte {
|
||||
return []byte(strconv.Itoa(v))
|
||||
}
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
service := BaseDataService[testObject, int]{
|
||||
Bucket: "testBucket",
|
||||
Connection: mockConnection{store: make(map[int]testObject)},
|
||||
}
|
||||
|
||||
data := []testObject{
|
||||
{ID: 1, Value: 1},
|
||||
{ID: 2, Value: 2},
|
||||
{ID: 3, Value: 3},
|
||||
{ID: 4, Value: 4},
|
||||
{ID: 5, Value: 5},
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
err := service.Update(item.ID, &item)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// ReadAll without predicates
|
||||
result, err := service.ReadAll()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := append([]testObject{}, data...)
|
||||
|
||||
require.ElementsMatch(t, expected, result)
|
||||
|
||||
// ReadAll with predicates
|
||||
hasLowID := func(obj testObject) bool { return obj.ID < 3 }
|
||||
isEven := func(obj testObject) bool { return obj.Value%2 == 0 }
|
||||
|
||||
result, err = service.ReadAll(hasLowID, isEven)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected = slicesx.Filter(expected, hasLowID)
|
||||
expected = slicesx.Filter(expected, isEven)
|
||||
|
||||
require.ElementsMatch(t, expected, result)
|
||||
}
|
||||
@@ -28,38 +28,13 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
|
||||
return &element, nil
|
||||
}
|
||||
|
||||
func (service BaseDataServiceTx[T, I]) Exists(ID I) (bool, error) {
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
|
||||
return service.Tx.KeyExists(service.Bucket, identifier)
|
||||
}
|
||||
|
||||
// ReadAll retrieves all the elements that satisfy all the provided predicates.
|
||||
func (service BaseDataServiceTx[T, I]) ReadAll(predicates ...func(T) bool) ([]T, error) {
|
||||
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
|
||||
var collection = make([]T, 0)
|
||||
|
||||
if len(predicates) == 0 {
|
||||
return collection, service.Tx.GetAll(
|
||||
service.Bucket,
|
||||
new(T),
|
||||
AppendFn(&collection),
|
||||
)
|
||||
}
|
||||
|
||||
filterFn := func(element T) bool {
|
||||
for _, p := range predicates {
|
||||
if !p(element) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return collection, service.Tx.GetAll(
|
||||
service.Bucket,
|
||||
new(T),
|
||||
FilterFn(&collection, filterFn),
|
||||
AppendFn(&collection),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type Service struct {
|
||||
connection portainer.Connection
|
||||
idxVersion map[portainer.EdgeStackID]int
|
||||
mu sync.RWMutex
|
||||
cacheInvalidationFn func(portainer.Transaction, portainer.EdgeStackID)
|
||||
cacheInvalidationFn func(portainer.EdgeStackID)
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -23,7 +23,7 @@ func (service *Service) BucketName() string {
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.Transaction, portainer.EdgeStackID)) (*Service, error) {
|
||||
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,7 +36,7 @@ func NewService(connection portainer.Connection, cacheInvalidationFn func(portai
|
||||
}
|
||||
|
||||
if s.cacheInvalidationFn == nil {
|
||||
s.cacheInvalidationFn = func(portainer.Transaction, portainer.EdgeStackID) {}
|
||||
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
|
||||
}
|
||||
|
||||
es, err := s.EdgeStacks()
|
||||
@@ -106,7 +106,7 @@ func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.Ed
|
||||
|
||||
service.mu.Lock()
|
||||
service.idxVersion[id] = edgeStack.Version
|
||||
service.cacheInvalidationFn(service.connection, id)
|
||||
service.cacheInvalidationFn(id)
|
||||
service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -125,7 +125,7 @@ func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *por
|
||||
}
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(service.connection, ID)
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc
|
||||
updateFunc(edgeStack)
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(service.connection, ID)
|
||||
service.cacheInvalidationFn(ID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
|
||||
delete(service.idxVersion, ID)
|
||||
|
||||
service.cacheInvalidationFn(service.connection, ID)
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeSta
|
||||
var stack portainer.EdgeStack
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
if err := service.tx.GetObject(BucketName, identifier, &stack); err != nil {
|
||||
err := service.tx.GetObject(BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -64,17 +65,18 @@ func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
|
||||
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
edgeStack.ID = id
|
||||
|
||||
if err := service.tx.CreateObjectWithId(
|
||||
err := service.tx.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeStack.ID),
|
||||
edgeStack,
|
||||
); err != nil {
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.idxVersion[id] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(service.tx, id)
|
||||
service.service.cacheInvalidationFn(id)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -87,12 +89,13 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
if err := service.tx.UpdateObject(BucketName, identifier, edgeStack); err != nil {
|
||||
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.idxVersion[ID] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(service.tx, ID)
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -116,13 +119,14 @@ func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(service.service.idxVersion, ID)
|
||||
|
||||
service.service.cacheInvalidationFn(service.tx, ID)
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package edgestackstatus
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
var _ dataservices.EdgeStackStatusService = &Service{}
|
||||
|
||||
const BucketName = "edge_stack_status"
|
||||
|
||||
type Service struct {
|
||||
conn portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
if err := connection.SetServiceName(BucketName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{conn: connection}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: s,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
|
||||
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Create(edgeStackID, endpointID, status)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
|
||||
var element *portainer.EdgeStackStatusForEnv
|
||||
|
||||
return element, s.conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
element, err = s.Tx(tx).Read(edgeStackID, endpointID)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
|
||||
var collection = make([]portainer.EdgeStackStatusForEnv, 0)
|
||||
|
||||
return collection, s.conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
collection, err = s.Tx(tx).ReadAll(edgeStackID)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
|
||||
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Update(edgeStackID, endpointID, status)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
|
||||
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Delete(edgeStackID, endpointID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAll(edgeStackID portainer.EdgeStackID) error {
|
||||
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).DeleteAll(edgeStackID)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
|
||||
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return s.Tx(tx).Clear(edgeStackID, relatedEnvironmentsIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) key(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) []byte {
|
||||
return append(s.conn.ConvertToKey(int(edgeStackID)), s.conn.ConvertToKey(int(endpointID))...)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package edgestackstatus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
var _ dataservices.EdgeStackStatusService = &Service{}
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
|
||||
identifier := service.service.key(edgeStackID, endpointID)
|
||||
return service.tx.CreateObjectWithStringId(BucketName, identifier, status)
|
||||
}
|
||||
|
||||
func (s ServiceTx) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
|
||||
var status portainer.EdgeStackStatusForEnv
|
||||
identifier := s.service.key(edgeStackID, endpointID)
|
||||
|
||||
if err := s.tx.GetObject(BucketName, identifier, &status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (s ServiceTx) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
|
||||
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
|
||||
|
||||
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
|
||||
|
||||
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
|
||||
}
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
func (s ServiceTx) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
|
||||
identifier := s.service.key(edgeStackID, endpointID)
|
||||
return s.tx.UpdateObject(BucketName, identifier, status)
|
||||
}
|
||||
|
||||
func (s ServiceTx) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
|
||||
identifier := s.service.key(edgeStackID, endpointID)
|
||||
return s.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
func (s ServiceTx) DeleteAll(edgeStackID portainer.EdgeStackID) error {
|
||||
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
|
||||
|
||||
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
|
||||
|
||||
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
|
||||
return fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
|
||||
}
|
||||
|
||||
for _, status := range statuses {
|
||||
if err := s.tx.DeleteObject(BucketName, s.service.key(edgeStackID, status.EndpointID)); err != nil {
|
||||
return fmt.Errorf("unable to delete EdgeStackStatus for EdgeStack %d and Endpoint %d: %w", edgeStackID, status.EndpointID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ServiceTx) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
|
||||
for _, envID := range relatedEnvironmentsIDs {
|
||||
existingStatus, err := s.Read(edgeStackID, envID)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return fmt.Errorf("unable to retrieve status for environment %d: %w", envID, err)
|
||||
}
|
||||
|
||||
var deploymentInfo portainer.StackDeploymentInfo
|
||||
if existingStatus != nil {
|
||||
deploymentInfo = existingStatus.DeploymentInfo
|
||||
}
|
||||
|
||||
if err := s.Update(edgeStackID, envID, &portainer.EdgeStackStatusForEnv{
|
||||
EndpointID: envID,
|
||||
Status: []portainer.EdgeStackDeploymentStatus{},
|
||||
DeploymentInfo: deploymentInfo,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package endpointrelation
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
@@ -15,15 +13,11 @@ const BucketName = "endpoint_relations"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) relation data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
endpointRelationsCache []portainer.EndpointRelation
|
||||
mu sync.Mutex
|
||||
connection portainer.Connection
|
||||
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
}
|
||||
|
||||
var _ dataservices.EndpointRelationService = &Service{}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
@@ -82,10 +76,6 @@ func (service *Service) Create(endpointRelation *portainer.EndpointRelation) err
|
||||
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
service.mu.Lock()
|
||||
service.endpointRelationsCache = nil
|
||||
service.mu.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,27 +92,11 @@ func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID,
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.mu.Lock()
|
||||
service.endpointRelationsCache = nil
|
||||
service.mu.Unlock()
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).AddEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).RemoveEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
@@ -134,15 +108,27 @@ func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID)
|
||||
return err
|
||||
}
|
||||
|
||||
service.mu.Lock()
|
||||
service.endpointRelationsCache = nil
|
||||
service.mu.Unlock()
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ type ServiceTx struct {
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
var _ dataservices.EndpointRelationService = &ServiceTx{}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
@@ -47,10 +45,6 @@ func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) er
|
||||
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.endpointRelationsCache = nil
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -67,75 +61,11 @@ func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID,
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.endpointRelationsCache = nil
|
||||
service.service.mu.Unlock()
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
|
||||
for _, endpointID := range endpointIDs {
|
||||
rel, err := service.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel.EdgeStacks[edgeStackID] = true
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err = service.tx.UpdateObject(BucketName, identifier, rel)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.endpointRelationsCache = nil
|
||||
service.service.mu.Unlock()
|
||||
|
||||
if err := service.service.updateStackFnTx(service.tx, edgeStackID, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments += len(endpointIDs)
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("could not update the number of deployments")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
|
||||
for _, endpointID := range endpointIDs {
|
||||
rel, err := service.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(rel.EdgeStacks, edgeStackID)
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err = service.tx.UpdateObject(BucketName, identifier, rel)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.endpointRelationsCache = nil
|
||||
service.service.mu.Unlock()
|
||||
|
||||
if err := service.service.updateStackFnTx(service.tx, edgeStackID, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments -= len(endpointIDs)
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("could not update the number of deployments")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
@@ -147,44 +77,27 @@ func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID)
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.endpointRelationsCache = nil
|
||||
service.service.mu.Unlock()
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.cachedEndpointRelations()
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
if _, ok := rel.EdgeStacks[edgeStackID]; ok {
|
||||
cache.Del(rel.EndpointID)
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service ServiceTx) cachedEndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||
service.service.mu.Lock()
|
||||
defer service.service.mu.Unlock()
|
||||
|
||||
if service.service.endpointRelationsCache == nil {
|
||||
var err error
|
||||
service.service.endpointRelationsCache, err = service.EndpointRelations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return service.service.endpointRelationsCache, nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
@@ -220,7 +133,6 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
|
||||
}
|
||||
|
||||
numDeployments := 0
|
||||
|
||||
for _, r := range relations {
|
||||
for sId, enabled := range r.EdgeStacks {
|
||||
if enabled && sId == refStackId {
|
||||
|
||||
@@ -12,7 +12,6 @@ type (
|
||||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
EdgeStack() EdgeStackService
|
||||
EdgeStackStatus() EdgeStackStatusService
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
@@ -40,8 +39,8 @@ type (
|
||||
Open() (newStore bool, err error)
|
||||
Init() error
|
||||
Close() error
|
||||
UpdateTx(func(tx DataStoreTx) error) error
|
||||
ViewTx(func(tx DataStoreTx) error) error
|
||||
UpdateTx(func(DataStoreTx) error) error
|
||||
ViewTx(func(DataStoreTx) error) error
|
||||
MigrateData() error
|
||||
Rollback(force bool) error
|
||||
CheckCurrentEdition() error
|
||||
@@ -90,16 +89,6 @@ type (
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
EdgeStackStatusService interface {
|
||||
Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
|
||||
Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error)
|
||||
ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error)
|
||||
Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
|
||||
Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error
|
||||
DeleteAll(edgeStackID portainer.EdgeStackID) error
|
||||
Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error
|
||||
}
|
||||
|
||||
// EndpointService represents a service for managing environment(endpoint) data
|
||||
EndpointService interface {
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
@@ -126,8 +115,6 @@ type (
|
||||
EndpointRelation(EndpointID portainer.EndpointID) (*portainer.EndpointRelation, error)
|
||||
Create(endpointRelation *portainer.EndpointRelation) error
|
||||
UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error
|
||||
AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error
|
||||
RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error
|
||||
DeleteEndpointRelation(EndpointID portainer.EndpointID) error
|
||||
BucketName() string
|
||||
}
|
||||
@@ -170,7 +157,6 @@ type (
|
||||
|
||||
SnapshotService interface {
|
||||
BaseCRUD[portainer.Snapshot, portainer.EndpointID]
|
||||
ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error)
|
||||
}
|
||||
|
||||
// SSLSettingsService represents a service for managing application settings
|
||||
|
||||
@@ -38,33 +38,3 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
func (service *Service) Create(snapshot *portainer.Snapshot) error {
|
||||
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
|
||||
func (service *Service) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error) {
|
||||
var snapshot *portainer.Snapshot
|
||||
|
||||
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
snapshot, err = service.Tx(tx).ReadWithoutSnapshotRaw(ID)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return snapshot, err
|
||||
}
|
||||
|
||||
func (service *Service) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
|
||||
var snapshot *portainer.SnapshotRawMessage
|
||||
|
||||
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
var err error
|
||||
snapshot, err = service.Tx(tx).ReadRawMessage(ID)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return snapshot, err
|
||||
}
|
||||
|
||||
func (service *Service) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
|
||||
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
|
||||
@@ -12,42 +12,3 @@ type ServiceTx struct {
|
||||
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
|
||||
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
|
||||
func (service ServiceTx) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error) {
|
||||
var snapshot struct {
|
||||
Docker *struct {
|
||||
X struct{} `json:"DockerSnapshotRaw"`
|
||||
*portainer.DockerSnapshot
|
||||
} `json:"Docker"`
|
||||
|
||||
portainer.Snapshot
|
||||
}
|
||||
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
|
||||
if err := service.Tx.GetObject(service.Bucket, identifier, &snapshot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if snapshot.Docker != nil {
|
||||
snapshot.Snapshot.Docker = snapshot.Docker.DockerSnapshot
|
||||
}
|
||||
|
||||
return &snapshot.Snapshot, nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
|
||||
var snapshot = portainer.SnapshotRawMessage{}
|
||||
|
||||
identifier := service.Connection.ConvertToKey(int(ID))
|
||||
|
||||
if err := service.Tx.GetObject(service.Bucket, identifier, &snapshot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
|
||||
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
|
||||
@@ -40,11 +40,13 @@ func (store *Store) MigrateData() error {
|
||||
}
|
||||
|
||||
// before we alter anything in the DB, create a backup
|
||||
if _, err := store.Backup(""); err != nil {
|
||||
_, err = store.Backup("")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while backing up database")
|
||||
}
|
||||
|
||||
if err := store.FailSafeMigrate(migrator, version); err != nil {
|
||||
err = store.FailSafeMigrate(migrator, version)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
||||
@@ -83,7 +85,6 @@ func (store *Store) newMigratorParameters(version *models.Version, flags *portai
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EdgeStackStatusService: store.EdgeStackStatusService,
|
||||
EdgeJobService: store.EdgeJobService,
|
||||
TunnelServerService: store.TunnelServerService,
|
||||
PendingActionsService: store.PendingActionsService,
|
||||
@@ -139,7 +140,8 @@ func (store *Store) connectionRollback(force bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.Restore(); err != nil {
|
||||
err := store.Restore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateEdgeStacksStatuses_2_31_0() error {
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for envID, status := range edgeStack.Status {
|
||||
if err := m.edgeStackStatusService.Create(edgeStack.ID, envID, &portainer.EdgeStackStatusForEnv{
|
||||
EndpointID: envID,
|
||||
Status: status.Status,
|
||||
DeploymentInfo: status.DeploymentInfo,
|
||||
ReadyRePullImage: status.ReadyRePullImage,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
edgeStack.Status = nil
|
||||
|
||||
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
perrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
func (m *Migrator) addEndpointRelationForEdgeAgents_2_32_0() error {
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
_, err := m.endpointRelationService.EndpointRelation(endpoint.ID)
|
||||
if err != nil && errors.Is(err, perrors.ErrObjectNotFound) {
|
||||
relation := &portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
EdgeStacks: make(map[portainer.EdgeStackID]bool),
|
||||
}
|
||||
|
||||
if err := m.endpointRelationService.Create(relation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -94,10 +94,6 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
||||
continue
|
||||
}
|
||||
|
||||
if environmentStatus.Details == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
statusArray := []portainer.EdgeStackDeploymentStatus{}
|
||||
if environmentStatus.Details.Pending {
|
||||
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
|
||||
|
||||
@@ -18,7 +18,8 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
|
||||
if err := m.resourceControlService.Update(resourceControl.ID, &resourceControl); err != nil {
|
||||
err := m.resourceControlService.Update(resourceControl.ID, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -41,8 +42,8 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
|
||||
|
||||
if err := m.userService.Update(user.ID, &user); err != nil {
|
||||
err = m.userService.Update(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -51,47 +52,38 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointAdministratorRole.Priority = 1
|
||||
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||
|
||||
if err := m.roleService.Update(endpointAdministratorRole.ID, endpointAdministratorRole); err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.roleService.Update(endpointAdministratorRole.ID, endpointAdministratorRole)
|
||||
|
||||
helpDeskRole, err := m.roleService.Read(portainer.RoleID(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(helpDeskRole.ID, helpDeskRole); err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.roleService.Update(helpDeskRole.ID, helpDeskRole)
|
||||
|
||||
standardUserRole, err := m.roleService.Read(portainer.RoleID(3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(standardUserRole.ID, standardUserRole); err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.roleService.Update(standardUserRole.ID, standardUserRole)
|
||||
|
||||
readOnlyUserRole, err := m.roleService.Read(portainer.RoleID(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
if err := m.roleService.Update(readOnlyUserRole.ID, readOnlyUserRole); err != nil {
|
||||
err = m.roleService.Update(readOnlyUserRole.ID, readOnlyUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,6 @@ func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for endpointId, status := range edgeStack.Status {
|
||||
if status.Details == nil {
|
||||
status.Details = &portainer.EdgeStackStatusDetails{}
|
||||
}
|
||||
|
||||
switch status.Type {
|
||||
case portainer.EdgeStackStatusPending:
|
||||
status.Details.Pending = true
|
||||
@@ -97,10 +93,10 @@ func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
||||
edgeStack.Status[endpointId] = status
|
||||
}
|
||||
|
||||
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package migrator
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices/dockerhub"
|
||||
"github.com/portainer/portainer/api/dataservices/edgejob"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestack"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
|
||||
"github.com/portainer/portainer/api/dataservices/endpoint"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
@@ -27,8 +27,6 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/user"
|
||||
"github.com/portainer/portainer/api/dataservices/version"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -58,7 +56,6 @@ type (
|
||||
authorizationService *authorization.Service
|
||||
dockerhubService *dockerhub.Service
|
||||
edgeStackService *edgestack.Service
|
||||
edgeStackStatusService *edgestackstatus.Service
|
||||
edgeJobService *edgejob.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
pendingActionsService *pendingactions.Service
|
||||
@@ -87,7 +84,6 @@ type (
|
||||
AuthorizationService *authorization.Service
|
||||
DockerhubService *dockerhub.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeStackStatusService *edgestackstatus.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
PendingActionsService *pendingactions.Service
|
||||
@@ -118,7 +114,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||
authorizationService: parameters.AuthorizationService,
|
||||
dockerhubService: parameters.DockerhubService,
|
||||
edgeStackService: parameters.EdgeStackService,
|
||||
edgeStackStatusService: parameters.EdgeStackStatusService,
|
||||
edgeJobService: parameters.EdgeJobService,
|
||||
TunnelServerService: parameters.TunnelServerService,
|
||||
pendingActionsService: parameters.PendingActionsService,
|
||||
@@ -247,10 +242,6 @@ func (m *Migrator) initMigrations() {
|
||||
m.migratePendingActionsDataForDB130,
|
||||
)
|
||||
|
||||
m.addMigrations("2.31.0", m.migrateEdgeStacksStatuses_2_31_0)
|
||||
|
||||
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
|
||||
|
||||
// Add new migrations above...
|
||||
// One function per migration, each versions migration funcs in the same file.
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/portainer/portainer/api/dataservices/edgegroup"
|
||||
"github.com/portainer/portainer/api/dataservices/edgejob"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestack"
|
||||
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
|
||||
"github.com/portainer/portainer/api/dataservices/endpoint"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
@@ -40,8 +39,6 @@ import (
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
var _ dataservices.DataStore = &Store{}
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
@@ -54,7 +51,6 @@ type Store struct {
|
||||
EdgeGroupService *edgegroup.Service
|
||||
EdgeJobService *edgejob.Service
|
||||
EdgeStackService *edgestack.Service
|
||||
EdgeStackStatusService *edgestackstatus.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
@@ -104,21 +100,13 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.EndpointRelationService = endpointRelationService
|
||||
|
||||
edgeStackService, err := edgestack.NewService(store.connection, func(tx portainer.Transaction, ID portainer.EdgeStackID) {
|
||||
endpointRelationService.Tx(tx).InvalidateEdgeCacheForEdgeStack(ID)
|
||||
})
|
||||
edgeStackService, err := edgestack.NewService(store.connection, endpointRelationService.InvalidateEdgeCacheForEdgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EdgeStackService = edgeStackService
|
||||
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx)
|
||||
|
||||
edgeStackStatusService, err := edgestackstatus.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.EdgeStackStatusService = edgeStackStatusService
|
||||
|
||||
edgeGroupService, err := edgegroup.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -279,10 +267,6 @@ func (store *Store) EdgeStack() dataservices.EdgeStackService {
|
||||
return store.EdgeStackService
|
||||
}
|
||||
|
||||
func (store *Store) EdgeStackStatus() dataservices.EdgeStackStatusService {
|
||||
return store.EdgeStackStatusService
|
||||
}
|
||||
|
||||
// Environment(Endpoint) gives access to the Environment(Endpoint) data management layer
|
||||
func (store *Store) Endpoint() dataservices.EndpointService {
|
||||
return store.EndpointService
|
||||
|
||||
@@ -32,10 +32,6 @@ func (tx *StoreTx) EdgeStack() dataservices.EdgeStackService {
|
||||
return tx.store.EdgeStackService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) EdgeStackStatus() dataservices.EdgeStackStatusService {
|
||||
return tx.store.EdgeStackStatusService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) Endpoint() dataservices.EndpointService {
|
||||
return tx.store.EndpointService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
}
|
||||
],
|
||||
"edge_stack": null,
|
||||
"edge_stack_status": null,
|
||||
"edgegroups": null,
|
||||
"edgejobs": null,
|
||||
"endpoint_groups": [
|
||||
@@ -121,10 +120,6 @@
|
||||
"Ecr": {
|
||||
"Region": ""
|
||||
},
|
||||
"Github": {
|
||||
"OrganisationName": "",
|
||||
"UseOrganisation": false
|
||||
},
|
||||
"Gitlab": {
|
||||
"InstanceURL": "",
|
||||
"ProjectId": 0,
|
||||
@@ -615,7 +610,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.32.0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.25.1",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -683,11 +678,14 @@
|
||||
"Images": null,
|
||||
"Info": {
|
||||
"Architecture": "",
|
||||
"BridgeNfIp6tables": false,
|
||||
"BridgeNfIptables": false,
|
||||
"CDISpecDirs": null,
|
||||
"CPUSet": false,
|
||||
"CPUShares": false,
|
||||
"CgroupDriver": "",
|
||||
"ContainerdCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Containers": 0,
|
||||
@@ -711,6 +709,7 @@
|
||||
"IndexServerAddress": "",
|
||||
"InitBinary": "",
|
||||
"InitCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Isolation": "",
|
||||
@@ -739,6 +738,7 @@
|
||||
},
|
||||
"RegistryConfig": null,
|
||||
"RuncCommit": {
|
||||
"Expected": "",
|
||||
"ID": ""
|
||||
},
|
||||
"Runtimes": null,
|
||||
@@ -780,7 +780,6 @@
|
||||
"ImageCount": 9,
|
||||
"IsPodman": false,
|
||||
"NodeCount": 0,
|
||||
"PerformanceMetrics": null,
|
||||
"RunningContainerCount": 5,
|
||||
"ServiceCount": 0,
|
||||
"StackCount": 2,
|
||||
@@ -944,7 +943,7 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.32.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.25.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
var validate *validator.Validate
|
||||
|
||||
func ValidateLDAPSettings(ldp *portainer.LDAPSettings) error {
|
||||
validate = validator.New()
|
||||
registerValidationMethods(validate)
|
||||
|
||||
return validate.Struct(ldp)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func TestValidateLDAPSettings(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ldap portainer.LDAPSettings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Empty LDAP Settings",
|
||||
ldap: portainer.LDAPSettings{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "With URL",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
URL: "192.168.0.1:323",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Validate URL and URLs",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
URL: "192.168.0.1:323",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "validate client ldap",
|
||||
ldap: portainer.LDAPSettings{
|
||||
AnonymousMode: false,
|
||||
ReaderDN: "CN=LDAP API Service Account",
|
||||
Password: "Qu**dfUUU**",
|
||||
URL: "aukdc15.pgc.co:389",
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateLDAPSettings(&tt.ldap)
|
||||
if (err == nil) == tt.wantErr {
|
||||
t.Errorf("No error expected but got %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
func registerValidationMethods(v *validator.Validate) {
|
||||
v.RegisterValidation("validate_bool", ValidateBool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation methods below are being used for custom validation
|
||||
*/
|
||||
func ValidateBool(fl validator.FieldLevel) bool {
|
||||
_, ok := fl.Field().Interface().(bool)
|
||||
return ok
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package client
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -73,6 +73,19 @@ func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func CreateClientFromEnv() (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.FromEnv,
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
|
||||
func CreateSimpleClient() (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithAPIVersionNegotiation(),
|
||||
)
|
||||
}
|
||||
|
||||
func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint, timeout)
|
||||
if err != nil {
|
||||
@@ -128,6 +141,7 @@ func createAgentClient(endpoint *portainer.Endpoint, endpointURL string, signatu
|
||||
|
||||
type NodeNameTransport struct {
|
||||
*http.Transport
|
||||
nodeNames map[string]string
|
||||
}
|
||||
|
||||
func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
@@ -162,19 +176,18 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
nodeNames, ok := req.Context().Value("nodeNames").(map[string]string)
|
||||
if ok {
|
||||
for idx, r := range rs {
|
||||
// as there is no way to differentiate the same image available in multiple nodes only by their ID
|
||||
// we append the index of the image in the payload response to match the node name later
|
||||
// from the image.Summary[] list returned by docker's client.ImageList()
|
||||
nodeNames[fmt.Sprintf("%s-%d", r.ID, idx)] = r.Portainer.Agent.NodeName
|
||||
}
|
||||
t.nodeNames = make(map[string]string)
|
||||
for _, r := range rs {
|
||||
t.nodeNames[r.ID] = r.Portainer.Agent.NodeName
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (t *NodeNameTransport) NodeNames() map[string]string {
|
||||
return maps.Clone(t.nodeNames)
|
||||
}
|
||||
|
||||
func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) {
|
||||
transport := &NodeNameTransport{
|
||||
Transport: &http.Transport{},
|
||||
|
||||
@@ -38,10 +38,10 @@ func NewClientWithRegistry(registryClient *RegistryClient, clientFactory *docker
|
||||
func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) {
|
||||
ctx, cancel := c.timeoutContext()
|
||||
defer cancel()
|
||||
|
||||
// Docker references with both a tag and digest are currently not supported
|
||||
if image.Tag != "" && image.Digest != "" {
|
||||
if err := image.TrimDigest(); err != nil {
|
||||
err := image.trimDigest()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) {
|
||||
// Retrieve remote digest through HEAD request
|
||||
rmDigest, err := docker.GetDigest(ctx, sysCtx, rmRef)
|
||||
if err != nil {
|
||||
// Fallback to public registry for hub
|
||||
// fallback to public registry for hub
|
||||
if image.HubLink != "" {
|
||||
rmDigest, err = docker.GetDigest(ctx, c.sysCtx, rmRef)
|
||||
if err == nil {
|
||||
@@ -131,7 +131,8 @@ func ParseRepoDigests(repoDigests []string) []digest.Digest {
|
||||
func ParseRepoTags(repoTags []string) []*Image {
|
||||
images := make([]*Image, 0)
|
||||
for _, repoTag := range repoTags {
|
||||
if image := ParseRepoTag(repoTag); image != nil {
|
||||
image := ParseRepoTag(repoTag)
|
||||
if image != nil {
|
||||
images = append(images, image)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +147,7 @@ func ParseRepoDigest(repoDigest string) digest.Digest {
|
||||
|
||||
d, err := digest.Parse(strings.Split(repoDigest, "@")[1])
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("digest", repoDigest).Msg("skip invalid repo item")
|
||||
log.Warn().Msgf("Skip invalid repo digest item: %s [error: %v]", repoDigest, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Image struct {
|
||||
Digest digest.Digest
|
||||
HubLink string
|
||||
named reference.Named
|
||||
Opts ParseImageOptions `json:"-"`
|
||||
opts ParseImageOptions
|
||||
}
|
||||
|
||||
// ParseImageOptions holds image options for parsing.
|
||||
@@ -43,10 +43,9 @@ func (i *Image) Name() string {
|
||||
// FullName return the real full name may include Tag or Digest of the image, Tag first.
|
||||
func (i *Image) FullName() string {
|
||||
if i.Tag == "" {
|
||||
return i.Name() + "@" + i.Digest.String()
|
||||
return fmt.Sprintf("%s@%s", i.Name(), i.Digest)
|
||||
}
|
||||
|
||||
return i.Name() + ":" + i.Tag
|
||||
return fmt.Sprintf("%s:%s", i.Name(), i.Tag)
|
||||
}
|
||||
|
||||
// String returns the string representation of an image, including Tag and Digest if existed.
|
||||
@@ -67,25 +66,22 @@ func (i *Image) Reference() string {
|
||||
func (i *Image) WithDigest(digest digest.Digest) (err error) {
|
||||
i.Digest = digest
|
||||
i.named, err = reference.WithDigest(i.named, digest)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Image) WithTag(tag string) (err error) {
|
||||
i.Tag = tag
|
||||
i.named, err = reference.WithTag(i.named, tag)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *Image) TrimDigest() error {
|
||||
func (i *Image) trimDigest() error {
|
||||
i.Digest = ""
|
||||
named, err := ParseImage(ParseImageOptions{Name: i.FullName()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.named = &named
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -96,12 +92,11 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
|
||||
if err != nil {
|
||||
return Image{}, errors.Wrapf(err, "parsing image %s failed", parseOpts.Name)
|
||||
}
|
||||
|
||||
// Add the latest lag if they did not provide one.
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
i := Image{
|
||||
Opts: parseOpts,
|
||||
opts: parseOpts,
|
||||
named: named,
|
||||
Domain: reference.Domain(named),
|
||||
Path: reference.Path(named),
|
||||
@@ -127,16 +122,15 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
|
||||
}
|
||||
|
||||
func (i *Image) hubLink() (string, error) {
|
||||
if i.Opts.HubTpl != "" {
|
||||
if i.opts.HubTpl != "" {
|
||||
var out bytes.Buffer
|
||||
tmpl, err := template.New("tmpl").
|
||||
Option("missingkey=error").
|
||||
Parse(i.Opts.HubTpl)
|
||||
Parse(i.opts.HubTpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = tmpl.Execute(&out, i)
|
||||
|
||||
return out.String(), err
|
||||
}
|
||||
|
||||
@@ -148,7 +142,6 @@ func (i *Image) hubLink() (string, error) {
|
||||
prefix = "_"
|
||||
path = strings.Replace(i.Path, "library/", "", 1)
|
||||
}
|
||||
|
||||
return "https://hub.docker.com/" + prefix + "/" + path, nil
|
||||
case "docker.bintray.io", "jfrog-docker-reg2.bintray.io":
|
||||
return "https://bintray.com/jfrog/reg2/" + strings.ReplaceAll(i.Path, "/", "%3A"), nil
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestImageParser(t *testing.T) {
|
||||
})
|
||||
is.NoError(err, "")
|
||||
is.Equal("docker.io/portainer/portainer-ee:latest", image.FullName())
|
||||
is.Equal("portainer/portainer-ee", image.Opts.Name)
|
||||
is.Equal("portainer/portainer-ee", image.opts.Name)
|
||||
is.Equal("latest", image.Tag)
|
||||
is.Equal("portainer/portainer-ee", image.Path)
|
||||
is.Equal("docker.io", image.Domain)
|
||||
@@ -32,7 +32,7 @@ func TestImageParser(t *testing.T) {
|
||||
})
|
||||
is.NoError(err, "")
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.FullName())
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name)
|
||||
is.Equal("", image.Tag)
|
||||
is.Equal("k8s-minikube/kicbase", image.Path)
|
||||
is.Equal("gcr.io", image.Domain)
|
||||
@@ -49,7 +49,7 @@ func TestImageParser(t *testing.T) {
|
||||
})
|
||||
is.NoError(err, "")
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name)
|
||||
is.Equal("v0.0.30", image.Tag)
|
||||
is.Equal("k8s-minikube/kicbase", image.Path)
|
||||
is.Equal("gcr.io", image.Domain)
|
||||
@@ -71,7 +71,7 @@ func TestUpdateParsedImage(t *testing.T) {
|
||||
is.NoError(err, "")
|
||||
_ = image.WithTag("v0.0.31")
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.31", image.FullName())
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name)
|
||||
is.Equal("v0.0.31", image.Tag)
|
||||
is.Equal("k8s-minikube/kicbase", image.Path)
|
||||
is.Equal("gcr.io", image.Domain)
|
||||
@@ -89,7 +89,7 @@ func TestUpdateParsedImage(t *testing.T) {
|
||||
is.NoError(err, "")
|
||||
_ = image.WithDigest("sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b3")
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name)
|
||||
is.Equal("v0.0.30", image.Tag)
|
||||
is.Equal("k8s-minikube/kicbase", image.Path)
|
||||
is.Equal("gcr.io", image.Domain)
|
||||
@@ -105,9 +105,9 @@ func TestUpdateParsedImage(t *testing.T) {
|
||||
Name: "gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2",
|
||||
})
|
||||
is.NoError(err, "")
|
||||
_ = image.TrimDigest()
|
||||
_ = image.trimDigest()
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
|
||||
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name)
|
||||
is.Equal("v0.0.30", image.Tag)
|
||||
is.Equal("k8s-minikube/kicbase", image.Path)
|
||||
is.Equal("gcr.io", image.Domain)
|
||||
|
||||
@@ -29,7 +29,7 @@ func (c *RegistryClient) RegistryAuth(image Image) (string, string, error) {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
registry, err := findBestMatchRegistry(image.Opts.Name, registries)
|
||||
registry, err := findBestMatchRegistry(image.opts.Name, registries)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func (c *RegistryClient) EncodedRegistryAuth(image Image) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
registry, err := findBestMatchRegistry(image.Opts.Name, registries)
|
||||
registry, err := findBestMatchRegistry(image.opts.Name, registries)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+1
-6
@@ -64,14 +64,9 @@ type (
|
||||
|
||||
DeployerOptionsPayload struct {
|
||||
// Prune is a flag indicating if the agent must prune the containers or not when creating/updating an edge stack
|
||||
// This flag drives `docker compose up --remove-orphans` and `docker stack up --prune` options
|
||||
// This flag drives docker compose `--remove-orphans` and docker stack `--prune` options
|
||||
// Used only for EE
|
||||
Prune bool
|
||||
// RemoveVolumes is a flag indicating if the agent must remove the named volumes declared
|
||||
// in the compose file and anonymouse volumes attached to containers
|
||||
// This flag drives `docker compose down --volumes` option
|
||||
// Used only for EE
|
||||
RemoveVolumes bool
|
||||
}
|
||||
|
||||
// RegistryCredentials holds the credentials for a Docker registry.
|
||||
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type kubernetesMockDeployer struct {
|
||||
portainer.KubernetesDeployer
|
||||
}
|
||||
type kubernetesMockDeployer struct{}
|
||||
|
||||
// NewKubernetesDeployer creates a mock kubernetes deployer
|
||||
func NewKubernetesDeployer() *kubernetesMockDeployer {
|
||||
func NewKubernetesDeployer() portainer.KubernetesDeployer {
|
||||
return &kubernetesMockDeployer{}
|
||||
}
|
||||
|
||||
@@ -20,7 +18,3 @@ func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint
|
||||
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (deployer *kubernetesMockDeployer) Restart(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
@@ -10,17 +15,13 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/pkg/libkubectl"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultServerURL = "https://kubernetes.default.svc"
|
||||
)
|
||||
|
||||
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
|
||||
type KubernetesDeployer struct {
|
||||
binaryPath string
|
||||
dataStore dataservices.DataStore
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
@@ -30,8 +31,9 @@ type KubernetesDeployer struct {
|
||||
}
|
||||
|
||||
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) *KubernetesDeployer {
|
||||
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
|
||||
return &KubernetesDeployer{
|
||||
binaryPath: binaryPath,
|
||||
dataStore: datastore,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
signatureService: signatureService,
|
||||
@@ -76,56 +78,63 @@ func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *
|
||||
}
|
||||
|
||||
// Deploy upserts Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
|
||||
return deployer.command("apply", userID, endpoint, resources, namespace)
|
||||
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
// Remove deletes Kubernetes resources defined in manifest(s)
|
||||
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
|
||||
return deployer.command("delete", userID, endpoint, resources, namespace)
|
||||
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
|
||||
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
|
||||
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed generating a user token")
|
||||
}
|
||||
|
||||
serverURL := defaultServerURL
|
||||
command := path.Join(deployer.binaryPath, "kubectl")
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(deployer.binaryPath, "kubectl.exe")
|
||||
}
|
||||
|
||||
args := []string{"--token", token}
|
||||
if namespace != "" {
|
||||
args = append(args, "--namespace", namespace)
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||
url, proxy, err := deployer.getAgentURL(endpoint)
|
||||
if err != nil {
|
||||
return "", errors.WithMessage(err, "failed generating endpoint URL")
|
||||
}
|
||||
|
||||
defer proxy.Close()
|
||||
|
||||
serverURL = url
|
||||
args = append(args, "--server", url)
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
|
||||
client, err := libkubectl.NewClient(&libkubectl.ClientAccess{
|
||||
Token: token,
|
||||
ServerUrl: serverURL,
|
||||
}, namespace, "", true)
|
||||
if operation == "delete" {
|
||||
args = append(args, "--ignore-not-found=true")
|
||||
}
|
||||
|
||||
args = append(args, operation)
|
||||
for _, path := range manifestFiles {
|
||||
args = append(args, "-f", strings.TrimSpace(path))
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create kubectl client")
|
||||
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
|
||||
}
|
||||
|
||||
operations := map[string]func(context.Context, []string) (string, error){
|
||||
"apply": client.Apply,
|
||||
"delete": client.Delete,
|
||||
}
|
||||
|
||||
operationFunc, ok := operations[operation]
|
||||
if !ok {
|
||||
return "", errors.Errorf("unsupported operation: %s", operation)
|
||||
}
|
||||
|
||||
output, err := operationFunc(context.Background(), resources)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to execute kubectl %s command", operation)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockKubectlClient struct {
|
||||
applyFunc func(ctx context.Context, files []string) error
|
||||
deleteFunc func(ctx context.Context, files []string) error
|
||||
rolloutRestartFunc func(ctx context.Context, resources []string) error
|
||||
}
|
||||
|
||||
func (m *mockKubectlClient) Apply(ctx context.Context, files []string) error {
|
||||
if m.applyFunc != nil {
|
||||
return m.applyFunc(ctx, files)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockKubectlClient) Delete(ctx context.Context, files []string) error {
|
||||
if m.deleteFunc != nil {
|
||||
return m.deleteFunc(ctx, files)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockKubectlClient) RolloutRestart(ctx context.Context, resources []string) error {
|
||||
if m.rolloutRestartFunc != nil {
|
||||
return m.rolloutRestartFunc(ctx, resources)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testExecuteKubectlOperation(client *mockKubectlClient, operation string, manifestFiles []string) error {
|
||||
operations := map[string]func(context.Context, []string) error{
|
||||
"apply": client.Apply,
|
||||
"delete": client.Delete,
|
||||
"rollout-restart": client.RolloutRestart,
|
||||
}
|
||||
|
||||
operationFunc, ok := operations[operation]
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported operation: %s", operation)
|
||||
}
|
||||
|
||||
if err := operationFunc(context.Background(), manifestFiles); err != nil {
|
||||
return fmt.Errorf("failed to execute kubectl %s command: %w", operation, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_Apply_Success(t *testing.T) {
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
applyFunc: func(ctx context.Context, files []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"manifest1.yaml", "manifest2.yaml"}, files)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
manifests := []string{"manifest1.yaml", "manifest2.yaml"}
|
||||
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_Apply_Error(t *testing.T) {
|
||||
expectedErr := errors.New("kubectl apply failed")
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
applyFunc: func(ctx context.Context, files []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"error.yaml"}, files)
|
||||
return expectedErr
|
||||
},
|
||||
}
|
||||
|
||||
manifests := []string{"error.yaml"}
|
||||
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_Delete_Success(t *testing.T) {
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
deleteFunc: func(ctx context.Context, files []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"manifest1.yaml"}, files)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
manifests := []string{"manifest1.yaml"}
|
||||
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_Delete_Error(t *testing.T) {
|
||||
expectedErr := errors.New("kubectl delete failed")
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
deleteFunc: func(ctx context.Context, files []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"error.yaml"}, files)
|
||||
return expectedErr
|
||||
},
|
||||
}
|
||||
|
||||
manifests := []string{"error.yaml"}
|
||||
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_RolloutRestart_Success(t *testing.T) {
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"deployment/nginx"}, resources)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
resources := []string{"deployment/nginx"}
|
||||
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_RolloutRestart_Error(t *testing.T) {
|
||||
expectedErr := errors.New("kubectl rollout restart failed")
|
||||
called := false
|
||||
mockClient := &mockKubectlClient{
|
||||
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
|
||||
called = true
|
||||
assert.Equal(t, []string{"deployment/error"}, resources)
|
||||
return expectedErr
|
||||
},
|
||||
}
|
||||
|
||||
resources := []string{"deployment/error"}
|
||||
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), expectedErr.Error())
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestExecuteKubectlOperation_UnsupportedOperation(t *testing.T) {
|
||||
mockClient := &mockKubectlClient{}
|
||||
|
||||
err := testExecuteKubectlOperation(mockClient, "unsupported", []string{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported operation")
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
|
||||
return err
|
||||
}
|
||||
|
||||
args = append(args, "stack", "rm", "--detach=false", stack.Name)
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func copyFile(src, dst string) error {
|
||||
defer from.Close()
|
||||
|
||||
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := os.Create(dst)
|
||||
|
||||
@@ -841,11 +841,11 @@ func (service *Service) GetDefaultSSLCertsPath() (string, string) {
|
||||
}
|
||||
|
||||
func defaultMTLSCertPathUnderFileStore() (string, string, string) {
|
||||
caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename)
|
||||
certPath := JoinPaths(SSLCertPath, MTLSCertFilename)
|
||||
caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename)
|
||||
keyPath := JoinPaths(SSLCertPath, MTLSKeyFilename)
|
||||
|
||||
return caCertPath, certPath, keyPath
|
||||
return certPath, caCertPath, keyPath
|
||||
}
|
||||
|
||||
// GetDefaultChiselPrivateKeyPath returns the chisle private key path
|
||||
@@ -1014,45 +1014,26 @@ func CreateFile(path string, r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (service *Service) StoreMTLSCertificates(caCert, cert, key []byte) (string, string, string, error) {
|
||||
caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore()
|
||||
func (service *Service) StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error) {
|
||||
certPath, caCertPath, keyPath := defaultMTLSCertPathUnderFileStore()
|
||||
|
||||
r := bytes.NewReader(caCert)
|
||||
if err := service.createFileInStore(caCertPath, r); err != nil {
|
||||
r := bytes.NewReader(cert)
|
||||
err := service.createFileInStore(certPath, r)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
r = bytes.NewReader(cert)
|
||||
if err := service.createFileInStore(certPath, r); err != nil {
|
||||
r = bytes.NewReader(caCert)
|
||||
err = service.createFileInStore(caCertPath, r)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
r = bytes.NewReader(key)
|
||||
if err := service.createFileInStore(keyPath, r); err != nil {
|
||||
err = service.createFileInStore(keyPath, r)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(caCertPath), service.wrapFileStore(certPath), service.wrapFileStore(keyPath), nil
|
||||
}
|
||||
|
||||
func (service *Service) GetMTLSCertificates() (string, string, string, error) {
|
||||
caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore()
|
||||
|
||||
caCertPath = service.wrapFileStore(caCertPath)
|
||||
certPath = service.wrapFileStore(certPath)
|
||||
keyPath = service.wrapFileStore(keyPath)
|
||||
|
||||
paths := [...]string{caCertPath, certPath, keyPath}
|
||||
for _, path := range paths {
|
||||
exists, err := service.FileExists(path)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return "", "", "", fmt.Errorf("file %s does not exist", path)
|
||||
}
|
||||
}
|
||||
|
||||
return caCertPath, certPath, keyPath, nil
|
||||
return service.wrapFileStore(certPath), service.wrapFileStore(caCertPath), service.wrapFileStore(keyPath), nil
|
||||
}
|
||||
|
||||
@@ -15,19 +15,15 @@ type MultiFilterArgs []struct {
|
||||
}
|
||||
|
||||
// MultiFilterDirForPerDevConfigs filers the given dirEntries with multiple filter args, returns the merged entries for the given device
|
||||
func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs) ([]DirEntry, []string) {
|
||||
func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs) []DirEntry {
|
||||
var filteredDirEntries []DirEntry
|
||||
|
||||
var envFiles []string
|
||||
|
||||
for _, multiFilterArg := range multiFilterArgs {
|
||||
tmp, efs := FilterDirForPerDevConfigs(dirEntries, multiFilterArg.FilterKey, configPath, multiFilterArg.FilterType)
|
||||
tmp := FilterDirForPerDevConfigs(dirEntries, multiFilterArg.FilterKey, configPath, multiFilterArg.FilterType)
|
||||
filteredDirEntries = append(filteredDirEntries, tmp...)
|
||||
|
||||
envFiles = append(envFiles, efs...)
|
||||
}
|
||||
|
||||
return deduplicate(filteredDirEntries), envFiles
|
||||
return deduplicate(filteredDirEntries)
|
||||
}
|
||||
|
||||
func deduplicate(dirEntries []DirEntry) []DirEntry {
|
||||
@@ -36,7 +32,8 @@ func deduplicate(dirEntries []DirEntry) []DirEntry {
|
||||
marks := make(map[string]struct{})
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
if _, ok := marks[dirEntry.Name]; !ok {
|
||||
_, ok := marks[dirEntry.Name]
|
||||
if !ok {
|
||||
marks[dirEntry.Name] = struct{}{}
|
||||
deduplicatedDirEntries = append(deduplicatedDirEntries, dirEntry)
|
||||
}
|
||||
@@ -47,33 +44,34 @@ func deduplicate(dirEntries []DirEntry) []DirEntry {
|
||||
|
||||
// FilterDirForPerDevConfigs filers the given dirEntries, returns entries for the given device
|
||||
// For given configPath A/B/C, return entries:
|
||||
// 1. all entries outside of dir A/B/C
|
||||
// 2. For filterType file:
|
||||
// 1. all entries outside of dir A
|
||||
// 2. dir entries A, A/B, A/B/C
|
||||
// 3. For filterType file:
|
||||
// file entries: A/B/C/<deviceName> and A/B/C/<deviceName>.*
|
||||
// 3. For filterType dir:
|
||||
// 4. For filterType dir:
|
||||
// dir entry: A/B/C/<deviceName>
|
||||
// all entries: A/B/C/<deviceName>/*
|
||||
func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) ([]DirEntry, []string) {
|
||||
func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) []DirEntry {
|
||||
var filteredDirEntries []DirEntry
|
||||
|
||||
var envFiles []string
|
||||
|
||||
for _, dirEntry := range dirEntries {
|
||||
if shouldIncludeEntry(dirEntry, deviceName, configPath, filterType) {
|
||||
filteredDirEntries = append(filteredDirEntries, dirEntry)
|
||||
|
||||
if shouldParseEnvVars(dirEntry, deviceName, configPath, filterType) {
|
||||
envFiles = append(envFiles, dirEntry.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDirEntries, envFiles
|
||||
return filteredDirEntries
|
||||
}
|
||||
|
||||
func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool {
|
||||
|
||||
// Include all entries outside of dir A
|
||||
if !isInConfigDir(dirEntry, configPath) {
|
||||
if !isInConfigRootDir(dirEntry, configPath) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Include dir entries A, A/B, A/B/C
|
||||
if isParentDir(dirEntry, configPath) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -92,9 +90,21 @@ func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filter
|
||||
return false
|
||||
}
|
||||
|
||||
func isInConfigDir(dirEntry DirEntry, configPath string) bool {
|
||||
// return true if entry name starts with "A/B"
|
||||
return strings.HasPrefix(dirEntry.Name, appendTailSeparator(configPath))
|
||||
func isInConfigRootDir(dirEntry DirEntry, configPath string) bool {
|
||||
// get the first element of the configPath
|
||||
rootDir := strings.Split(configPath, string(os.PathSeparator))[0]
|
||||
|
||||
// return true if entry name starts with "A/"
|
||||
return strings.HasPrefix(dirEntry.Name, appendTailSeparator(rootDir))
|
||||
}
|
||||
|
||||
func isParentDir(dirEntry DirEntry, configPath string) bool {
|
||||
if dirEntry.IsFile {
|
||||
return false
|
||||
}
|
||||
|
||||
// return true for dir entries A, A/B, A/B/C
|
||||
return strings.HasPrefix(appendTailSeparator(configPath), appendTailSeparator(dirEntry.Name))
|
||||
}
|
||||
|
||||
func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool {
|
||||
@@ -128,15 +138,6 @@ func shouldIncludeDir(dirEntry DirEntry, deviceName, configPath string) bool {
|
||||
return strings.HasPrefix(dirEntry.Name, filterPrefix)
|
||||
}
|
||||
|
||||
func shouldParseEnvVars(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool {
|
||||
if !dirEntry.IsFile {
|
||||
return false
|
||||
}
|
||||
|
||||
return isInConfigDir(dirEntry, configPath) &&
|
||||
filepath.Base(dirEntry.Name) == deviceName+".env"
|
||||
}
|
||||
|
||||
func appendTailSeparator(path string) string {
|
||||
return fmt.Sprintf("%s%c", path, os.PathSeparator)
|
||||
}
|
||||
|
||||
@@ -4,17 +4,14 @@ import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
|
||||
f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, wantDirEntries []DirEntry) {
|
||||
t.Helper()
|
||||
|
||||
dirEntries, _ = MultiFilterDirForPerDevConfigs(dirEntries, configPath, multiFilterArgs)
|
||||
require.Equal(t, wantDirEntries, dirEntries)
|
||||
type args struct {
|
||||
dirEntries []DirEntry
|
||||
configPath string
|
||||
multiFilterArgs MultiFilterArgs
|
||||
}
|
||||
|
||||
baseDirEntries := []DirEntry{
|
||||
@@ -29,94 +26,67 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
|
||||
{"configs/folder2/config2", "", true, 420},
|
||||
}
|
||||
|
||||
// Filter file1
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]},
|
||||
)
|
||||
|
||||
// Filter folder1
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
)
|
||||
|
||||
// Filter file1 and folder1
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
)
|
||||
|
||||
// Filter file1 and file2
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"file1", portainer.PerDevConfigsTypeFile},
|
||||
{"file2", portainer.PerDevConfigsTypeFile},
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []DirEntry
|
||||
}{
|
||||
{
|
||||
name: "filter file1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]},
|
||||
},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
|
||||
)
|
||||
|
||||
// Filter folder1 and folder2
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"folder1", portainer.PerDevConfigsTypeDir},
|
||||
{"folder2", portainer.PerDevConfigsTypeDir},
|
||||
{
|
||||
name: "filter folder1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
},
|
||||
{
|
||||
name: "filter file1 and folder1",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
},
|
||||
{
|
||||
name: "filter file1 and file2",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"file1", portainer.PerDevConfigsTypeFile},
|
||||
{"file2", portainer.PerDevConfigsTypeFile},
|
||||
},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
|
||||
},
|
||||
{
|
||||
name: "filter folder1 and folder2",
|
||||
args: args{
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{
|
||||
{"folder1", portainer.PerDevConfigsTypeDir},
|
||||
{"folder2", portainer.PerDevConfigsTypeDir},
|
||||
},
|
||||
},
|
||||
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8]},
|
||||
},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8]},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMultiFilterDirForPerDevConfigsEnvFiles(t *testing.T) {
|
||||
f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, wantEnvFiles []string) {
|
||||
t.Helper()
|
||||
|
||||
_, envFiles := MultiFilterDirForPerDevConfigs(dirEntries, configPath, multiFilterArgs)
|
||||
require.Equal(t, wantEnvFiles, envFiles)
|
||||
}
|
||||
|
||||
baseDirEntries := []DirEntry{
|
||||
{".env", "", true, 420},
|
||||
{"docker-compose.yaml", "", true, 420},
|
||||
{"configs", "", false, 420},
|
||||
{"configs/edge-id/edge-id.env", "", true, 420},
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, MultiFilterDirForPerDevConfigs(tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs), "MultiFilterDirForPerDevConfigs(%v, %v, %v)", tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs)
|
||||
})
|
||||
}
|
||||
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"edge-id", portainer.PerDevConfigsTypeDir}},
|
||||
[]string{"configs/edge-id/edge-id.env"},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestIsInConfigDir(t *testing.T) {
|
||||
f := func(dirEntry DirEntry, configPath string, expect bool) {
|
||||
t.Helper()
|
||||
|
||||
actual := isInConfigDir(dirEntry, configPath)
|
||||
assert.Equal(t, expect, actual)
|
||||
}
|
||||
|
||||
f(DirEntry{Name: "edge-configs"}, "edge-configs", false)
|
||||
f(DirEntry{Name: "edge-configs_backup"}, "edge-configs", false)
|
||||
f(DirEntry{Name: "edge-configs/standalone-edge-agent-standard"}, "edge-configs", true)
|
||||
f(DirEntry{Name: "parent/edge-configs/"}, "edge-configs", false)
|
||||
f(DirEntry{Name: "edgestacktest"}, "edgestacktest/edge-configs", false)
|
||||
f(DirEntry{Name: "edgestacktest/edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false)
|
||||
f(DirEntry{Name: "edgestacktest/file1.conf"}, "edgestacktest/edge-configs", false)
|
||||
f(DirEntry{Name: "edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false)
|
||||
f(DirEntry{Name: "edgestacktest/edge-configs"}, "edgestacktest/edge-configs", false)
|
||||
f(DirEntry{Name: "edgestacktest/edge-configs/standalone-edge-agent-async"}, "edgestacktest/edge-configs", true)
|
||||
f(DirEntry{Name: "edgestacktest/edge-configs/abc.txt"}, "edgestacktest/edge-configs", true)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package update
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
)
|
||||
|
||||
func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error {
|
||||
@@ -17,7 +17,7 @@ func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error
|
||||
return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided")
|
||||
}
|
||||
|
||||
if autoUpdate.Webhook != "" && !validate.IsUUID(autoUpdate.Webhook) {
|
||||
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) {
|
||||
return httperrors.NewInvalidPayloadError("invalid Webhook format")
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -1,17 +1,19 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
)
|
||||
|
||||
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
|
||||
if len(repoConfig.URL) == 0 || !validate.IsURL(repoConfig.URL) {
|
||||
if len(repoConfig.URL) == 0 || !govalidator.IsURL(repoConfig.URL) {
|
||||
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
||||
return ValidateRepoAuthentication(repoConfig.Authentication)
|
||||
|
||||
}
|
||||
|
||||
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {
|
||||
|
||||
@@ -44,13 +44,13 @@ func (service *Service) executeDeviceAction(configuration portainer.OpenAMTConfi
|
||||
}
|
||||
|
||||
func parseAction(actionRaw string) (portainer.PowerState, error) {
|
||||
if strings.EqualFold(actionRaw, "power on") {
|
||||
switch strings.ToLower(actionRaw) {
|
||||
case "power on":
|
||||
return powerOnState, nil
|
||||
} else if strings.EqualFold(actionRaw, "power off") {
|
||||
case "power off":
|
||||
return powerOffState, nil
|
||||
} else if strings.EqualFold(actionRaw, "restart") {
|
||||
case "restart":
|
||||
return restartState, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unsupported device action %s", actionRaw)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(insecureSkipVerify bool) *Service {
|
||||
func NewService() *Service {
|
||||
tlsConfig := crypto.CreateTLSConfiguration()
|
||||
tlsConfig.InsecureSkipVerify = insecureSkipVerify
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
|
||||
return &Service{
|
||||
httpsClient: &http.Client{
|
||||
|
||||
+10
-48
@@ -2,7 +2,6 @@ package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -10,18 +9,11 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
gcsrf "github.com/gorilla/csrf"
|
||||
"github.com/rs/zerolog/log"
|
||||
gorillacsrf "github.com/gorilla/csrf"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
const csrfSkipHeader = "X-CSRF-Token-Skip"
|
||||
|
||||
func SkipCSRFToken(w http.ResponseWriter) {
|
||||
w.Header().Set(csrfSkipHeader, "1")
|
||||
}
|
||||
|
||||
func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, error) {
|
||||
func WithProtect(handler http.Handler) (http.Handler, error) {
|
||||
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
|
||||
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
|
||||
isDockerDesktopExtension := false
|
||||
@@ -36,12 +28,10 @@ func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, e
|
||||
return nil, fmt.Errorf("failed to generate CSRF token: %w", err)
|
||||
}
|
||||
|
||||
handler = gcsrf.Protect(
|
||||
handler = gorillacsrf.Protect(
|
||||
token,
|
||||
gcsrf.Path("/"),
|
||||
gcsrf.Secure(false),
|
||||
gcsrf.TrustedOrigins(trustedOrigins),
|
||||
gcsrf.ErrorHandler(withErrorHandler(trustedOrigins)),
|
||||
gorillacsrf.Path("/"),
|
||||
gorillacsrf.Secure(false),
|
||||
)(handler)
|
||||
|
||||
return withSkipCSRF(handler, isDockerDesktopExtension), nil
|
||||
@@ -52,14 +42,10 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
|
||||
sw := negroni.NewResponseWriter(w)
|
||||
|
||||
sw.Before(func(sw negroni.ResponseWriter) {
|
||||
if len(sw.Header().Get(csrfSkipHeader)) > 0 {
|
||||
sw.Header().Del(csrfSkipHeader)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if statusCode := sw.Status(); statusCode >= 200 && statusCode < 300 {
|
||||
sw.Header().Set("X-CSRF-Token", gcsrf.Token(r))
|
||||
statusCode := sw.Status()
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
csrfToken := gorillacsrf.Token(r)
|
||||
sw.Header().Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,33 +63,9 @@ func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Hand
|
||||
}
|
||||
|
||||
if skip {
|
||||
r = gcsrf.UnsafeSkipCheck(r)
|
||||
r = gorillacsrf.UnsafeSkipCheck(r)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func withErrorHandler(trustedOrigins []string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := gcsrf.FailureReason(r)
|
||||
|
||||
if errors.Is(err, gcsrf.ErrBadOrigin) || errors.Is(err, gcsrf.ErrBadReferer) || errors.Is(err, gcsrf.ErrNoReferer) {
|
||||
log.Error().Err(err).
|
||||
Str("request_url", r.URL.String()).
|
||||
Str("host", r.Host).
|
||||
Str("x_forwarded_proto", r.Header.Get("X-Forwarded-Proto")).
|
||||
Str("forwarded", r.Header.Get("Forwarded")).
|
||||
Str("origin", r.Header.Get("Origin")).
|
||||
Str("referer", r.Header.Get("Referer")).
|
||||
Strs("trusted_origins", trustedOrigins).
|
||||
Msg("Failed to validate Origin or Referer")
|
||||
}
|
||||
|
||||
http.Error(
|
||||
w,
|
||||
http.StatusText(http.StatusForbidden)+" - "+err.Error(),
|
||||
http.StatusForbidden,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -83,11 +82,6 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any existing user caches
|
||||
if user != nil {
|
||||
handler.KubernetesClientFactory.ClearUserClientCache(strconv.Itoa(int(user.ID)))
|
||||
}
|
||||
|
||||
if user != nil && isUserInitialAdmin(user) || settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
return handler.authenticateInternal(rw, user, payload.Password)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -24,18 +23,16 @@ type Handler struct {
|
||||
OAuthService portainer.OAuthService
|
||||
ProxyManager *proxy.Manager
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
KubernetesClientFactory *cli.ClientFactory
|
||||
passwordStrengthChecker security.PasswordStrengthChecker
|
||||
bouncer security.BouncerService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker, kubernetesClientFactory *cli.ClientFactory) *Handler {
|
||||
func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
passwordStrengthChecker: passwordStrengthChecker,
|
||||
bouncer: bouncer,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
|
||||
@@ -2,7 +2,6 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/logoutcontext"
|
||||
@@ -24,7 +23,6 @@ func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperro
|
||||
|
||||
if tokenData != nil {
|
||||
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
|
||||
handler.KubernetesClientFactory.ClearUserClientCache(strconv.Itoa(int(tokenData.ID)))
|
||||
logoutcontext.Cancel(tokenData.Token)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
@@ -228,7 +228,7 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
|
||||
if len(payload.Description) == 0 {
|
||||
return errors.New("Invalid custom template description")
|
||||
}
|
||||
if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
|
||||
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.RepositoryAuthentication && (len(payload.RepositoryUsername) == 0 || len(payload.RepositoryPassword) == 0) {
|
||||
@@ -482,3 +482,28 @@ func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*po
|
||||
|
||||
return customTemplate, nil
|
||||
}
|
||||
|
||||
// @id CustomTemplateCreate
|
||||
// @summary Create a custom template
|
||||
// @description Create a custom template.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags custom_templates
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json,multipart/form-data
|
||||
// @produce json
|
||||
// @param method query string true "method for creating template" Enums(string, file, repository)
|
||||
// @param body body object true "for body documentation see the relevant /custom_templates/{method} endpoint"
|
||||
// @success 200 {object} portainer.CustomTemplate
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @deprecated
|
||||
// @router /custom_templates [post]
|
||||
func deprecatedCustomTemplateCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
return "/custom_templates/create/" + method, nil
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
|
||||
customTemplates = filterByType(customTemplates, templateTypes)
|
||||
|
||||
if edge != nil {
|
||||
customTemplates = slicesx.FilterInPlace(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
|
||||
customTemplates = slicesx.Filter(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
|
||||
return customTemplate.EdgeTemplate == *edge
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
type customTemplateUpdatePayload struct {
|
||||
@@ -169,7 +170,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
customTemplate.EdgeTemplate = payload.EdgeTemplate
|
||||
|
||||
if payload.RepositoryURL != "" {
|
||||
if !validate.IsURL(payload.RepositoryURL) {
|
||||
if !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return httperror.BadRequest("Invalid repository URL. Must correspond to a valid URL format", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
)
|
||||
@@ -32,6 +33,7 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
|
||||
h.Handle("/custom_templates/create/{method}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/custom_templates", middlewares.Deprecated(h, deprecatedCustomTemplateCreateUrlParser)).Methods(http.MethodPost) // Deprecated
|
||||
h.Handle("/custom_templates",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet)
|
||||
h.Handle("/custom_templates/{id}",
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -117,12 +116,12 @@ func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) *httperror.H
|
||||
return err
|
||||
}
|
||||
|
||||
networks, err := cli.NetworkList(r.Context(), network.ListOptions{})
|
||||
networks, err := cli.NetworkList(r.Context(), types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker networks", err)
|
||||
}
|
||||
|
||||
networks, err = utils.FilterByResourceControl(tx, networks, portainer.NetworkResourceControl, context, func(c network.Summary) string {
|
||||
networks, err = utils.FilterByResourceControl(tx, networks, portainer.NetworkResourceControl, context, func(c types.NetworkResource) string {
|
||||
return c.Name
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/http/handler/docker/utils"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
@@ -47,16 +46,17 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
|
||||
return httpErr
|
||||
}
|
||||
|
||||
nodeNames := make(map[string]string)
|
||||
|
||||
// Pass the node names map to the context so the custom NodeNameTransport can use it
|
||||
ctx := context.WithValue(r.Context(), "nodeNames", nodeNames)
|
||||
|
||||
images, err := cli.ImageList(ctx, image.ListOptions{})
|
||||
images, err := cli.ImageList(r.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Docker images", err)
|
||||
}
|
||||
|
||||
// Extract the node name from the custom transport
|
||||
nodeNames := make(map[string]string)
|
||||
if t, ok := cli.HTTPClient().Transport.(*client.NodeNameTransport); ok {
|
||||
nodeNames = t.NodeNames()
|
||||
}
|
||||
|
||||
withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: withUsage", err)
|
||||
@@ -85,12 +85,8 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
imagesList[i] = ImageResponse{
|
||||
Created: image.Created,
|
||||
// Only works if the order of `images` is not changed between unmarshaling the agent's response
|
||||
// in NodeNameTransport.RoundTrip() (api/docker/client/client.go)
|
||||
// and docker's cli.ImageList()
|
||||
// As both functions unmarshal the same response body, the resulting array will be ordered the same way.
|
||||
NodeName: nodeNames[fmt.Sprintf("%s-%d", image.ID, i)],
|
||||
Created: image.Created,
|
||||
NodeName: nodeNames[image.ID],
|
||||
ID: image.ID,
|
||||
Size: image.Size,
|
||||
Tags: image.RepoTags,
|
||||
|
||||
@@ -24,6 +24,10 @@ type edgeGroupUpdatePayload struct {
|
||||
}
|
||||
|
||||
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
if len(payload.Name) == 0 {
|
||||
return errors.New("invalid Edge group name")
|
||||
}
|
||||
|
||||
if payload.Dynamic && len(payload.TagIDs) == 0 {
|
||||
return errors.New("tagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
@@ -31,7 +35,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EdgeGroupUpdate
|
||||
// @id EgeGroupUpdate
|
||||
// @summary Updates an EdgeGroup
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_groups
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
type edgeJobBasePayload struct {
|
||||
@@ -52,7 +53,7 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er
|
||||
return errors.New("invalid Edge job name")
|
||||
}
|
||||
|
||||
if !validate.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) {
|
||||
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) {
|
||||
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
@@ -135,7 +136,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
return errors.New("invalid Edge job name")
|
||||
}
|
||||
|
||||
if !validate.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
payload.Name = name
|
||||
@@ -270,3 +271,26 @@ func (handler *Handler) addAndPersistEdgeJob(tx dataservices.DataStoreTx, edgeJo
|
||||
|
||||
return tx.EdgeJob().CreateWithID(edgeJob.ID, edgeJob)
|
||||
}
|
||||
|
||||
// @id EdgeJobCreate
|
||||
// @summary Create an EdgeJob
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_jobs
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file, string)
|
||||
// @param body body object true "for body documentation see the relevant /edge_jobs/create/{method} endpoint"
|
||||
// @success 200 {object} portainer.EdgeGroup
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @failure 500
|
||||
// @deprecated
|
||||
// @router /edge_jobs [post]
|
||||
func deprecatedEdgeJobCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
return "/edge_jobs/create/" + method, nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
type edgeJobUpdatePayload struct {
|
||||
@@ -27,7 +28,7 @@ type edgeJobUpdatePayload struct {
|
||||
}
|
||||
|
||||
func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !validate.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("invalid Edge job name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -29,6 +30,8 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_jobs",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeJobCreateUrlParser)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/create/{method}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeJobCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_jobs/{id}",
|
||||
|
||||
@@ -55,3 +55,26 @@ func (handler *Handler) createSwarmStack(tx dataservices.DataStoreTx, method str
|
||||
|
||||
return nil, httperrors.NewInvalidPayloadError("Invalid value for query parameter: method. Value must be one of: string, repository or file")
|
||||
}
|
||||
|
||||
// @id EdgeStackCreate
|
||||
// @summary Create an EdgeStack
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_stacks
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param method query string true "Creation Method" Enums(file,string,repository)
|
||||
// @param body body object true "for body documentation see the relevant /edge_stacks/create/{method} endpoint"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @deprecated
|
||||
// @router /edge_stacks [post]
|
||||
func deprecatedEdgeStackCreateUrlParser(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return "", httperror.BadRequest("Invalid query parameter: method. Valid values are: file or string", err)
|
||||
}
|
||||
|
||||
return "/edge_stacks/create/" + method, nil
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ func (payload *edgeStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
// @router /edge_stacks/create/file [post]
|
||||
func (handler *Handler) createEdgeStackFromFileUpload(r *http.Request, tx dataservices.DataStoreTx, dryrun bool) (*portainer.EdgeStack, error) {
|
||||
payload := &edgeStackFromFileUploadPayload{}
|
||||
if err := payload.Validate(r); err != nil {
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/pkg/edge"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/validate"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
|
||||
return httperrors.NewInvalidPayloadError("Invalid stack name. Stack name must only consist of lowercase alpha characters, numbers, hyphens, or underscores as well as start with a lowercase character or number")
|
||||
}
|
||||
|
||||
if len(payload.RepositoryURL) == 0 || !validate.IsURL(payload.RepositoryURL) {
|
||||
if len(payload.RepositoryURL) == 0 || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
|
||||
// @router /edge_stacks/create/repository [post]
|
||||
func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dataservices.DataStoreTx, dryrun bool, userID portainer.UserID) (*portainer.EdgeStack, error) {
|
||||
var payload edgeStackFromGitRepositoryPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -136,9 +137,11 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
|
||||
}
|
||||
|
||||
func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStoreTx, stackFolder string, relatedEndpointIds []portainer.EndpointID, deploymentType portainer.EdgeStackDeploymentType, currentUserID portainer.UserID, repositoryConfig gittypes.RepoConfig) (composePath, manifestPath, projectPath string, err error) {
|
||||
if hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType); err != nil {
|
||||
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
|
||||
} else if hasWrongType {
|
||||
}
|
||||
if hasWrongType {
|
||||
return "", "", "", errors.New("edge stack with config do not match the environment type")
|
||||
}
|
||||
|
||||
@@ -150,7 +153,8 @@ func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStore
|
||||
repositoryPassword = repositoryConfig.Authentication.Password
|
||||
}
|
||||
|
||||
if err := handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify); err != nil {
|
||||
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,8 @@ func (payload *edgeStackFromStringPayload) Validate(r *http.Request) error {
|
||||
// @router /edge_stacks/create/string [post]
|
||||
func (handler *Handler) createEdgeStackFromFileContent(r *http.Request, tx dataservices.DataStoreTx, dryrun bool) (*portainer.EdgeStack, error) {
|
||||
var payload edgeStackFromStringPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -95,9 +96,11 @@ func (handler *Handler) createEdgeStackFromFileContent(r *http.Request, tx datas
|
||||
}
|
||||
|
||||
func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolder string, deploymentType portainer.EdgeStackDeploymentType, relatedEndpointIds []portainer.EndpointID, fileContent []byte) (composePath, manifestPath, projectPath string, err error) {
|
||||
if hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType); err != nil {
|
||||
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
|
||||
} else if hasWrongType {
|
||||
}
|
||||
if hasWrongType {
|
||||
return "", "", "", errors.New("edge stack with config do not match the environment type")
|
||||
}
|
||||
|
||||
@@ -121,6 +124,7 @@ func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolde
|
||||
}
|
||||
|
||||
return "", manifestPath, projectPath, nil
|
||||
|
||||
}
|
||||
|
||||
errMessage := fmt.Sprintf("invalid deployment type: %d", deploymentType)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
@@ -29,7 +28,9 @@ func TestCreateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpoint.ID,
|
||||
@@ -37,7 +38,9 @@ func TestCreateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
payload := edgeStackFromStringPayload{
|
||||
Name: "test-stack",
|
||||
@@ -47,14 +50,16 @@ func TestCreateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("JSON marshal error:", err)
|
||||
}
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
|
||||
// Create EdgeStack
|
||||
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", r)
|
||||
require.NoError(t, err)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
@@ -65,11 +70,15 @@ func TestCreateAndInspect(t *testing.T) {
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -81,7 +90,9 @@ func TestCreateAndInspect(t *testing.T) {
|
||||
|
||||
data = portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if payload.Name != data.Name {
|
||||
t.Fatalf("expected EdgeStack Name %s, found %s", payload.Name, data.Name)
|
||||
|
||||
@@ -3,7 +3,6 @@ package edgestacks
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
@@ -30,9 +29,10 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid edge stack identifier route variable", err)
|
||||
}
|
||||
|
||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.deleteEdgeStack(tx, portainer.EdgeStackID(edgeStackID))
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr
|
||||
@@ -52,14 +52,10 @@ func (handler *Handler) deleteEdgeStack(tx dataservices.DataStoreTx, edgeStackID
|
||||
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if err := handler.edgeStacksService.DeleteEdgeStack(tx, edgeStack.ID, edgeStack.EdgeGroups); err != nil {
|
||||
err = handler.edgeStacksService.DeleteEdgeStack(tx, edgeStack.ID, edgeStack.EdgeGroups)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete edge stack", err)
|
||||
}
|
||||
|
||||
stackFolder := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(edgeStack.ID)))
|
||||
if err := handler.FileService.RemoveDirectory(stackFolder); err != nil {
|
||||
return httperror.InternalServerError("Unable to remove edge stack project folder", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -10,8 +9,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Delete
|
||||
@@ -24,7 +21,9 @@ func TestDeleteAndInspect(t *testing.T) {
|
||||
|
||||
// Inspect
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -36,7 +35,9 @@ func TestDeleteAndInspect(t *testing.T) {
|
||||
|
||||
data := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&data)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if data.ID != edgeStack.ID {
|
||||
t.Fatalf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID)
|
||||
@@ -44,7 +45,9 @@ func TestDeleteAndInspect(t *testing.T) {
|
||||
|
||||
// Delete
|
||||
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -56,7 +59,9 @@ func TestDeleteAndInspect(t *testing.T) {
|
||||
|
||||
// Inspect
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -96,48 +101,3 @@ func TestDeleteInvalidEdgeStack(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteEdgeStack_RemoveProjectFolder(t *testing.T) {
|
||||
handler, rawAPIKey := setupHandler(t)
|
||||
|
||||
edgeGroup := createEdgeGroup(t, handler.DataStore)
|
||||
|
||||
payload := edgeStackFromStringPayload{
|
||||
Name: "test-stack",
|
||||
DeploymentType: portainer.EdgeStackDeploymentCompose,
|
||||
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
|
||||
StackFileContent: "version: '3.7'\nservices:\n test:\n image: test",
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := json.NewEncoder(&buf).Encode(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create
|
||||
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", &buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected a %d response, found: %d", http.StatusNoContent, rec.Code)
|
||||
}
|
||||
|
||||
assert.DirExists(t, handler.FileService.GetEdgeStackProjectPath("1"))
|
||||
|
||||
// Delete
|
||||
req, err = http.NewRequest(http.MethodDelete, "/edge_stacks/1", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected a %d response, found: %d", http.StatusNoContent, rec.Code)
|
||||
}
|
||||
|
||||
assert.NoDirExists(t, handler.FileService.GetEdgeStackProjectPath("1"))
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err != nil {
|
||||
return handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
|
||||
return handler.handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
fileName := stack.EntryPoint
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
@@ -31,38 +30,8 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err != nil {
|
||||
return handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
if err := fillEdgeStackStatus(handler.DataStore, edgeStack); err != nil {
|
||||
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
|
||||
return handler.handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStack)
|
||||
}
|
||||
|
||||
func fillEdgeStackStatus(tx dataservices.DataStoreTx, edgeStack *portainer.EdgeStack) error {
|
||||
status, err := tx.EdgeStackStatus().ReadAll(edgeStack.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStack.Status = make(map[portainer.EndpointID]portainer.EdgeStackStatus, len(status))
|
||||
|
||||
emptyStatus := make([]portainer.EdgeStackDeploymentStatus, 0)
|
||||
|
||||
for _, s := range status {
|
||||
if s.Status == nil {
|
||||
s.Status = emptyStatus
|
||||
}
|
||||
|
||||
edgeStack.Status[s.EndpointID] = portainer.EdgeStackStatus{
|
||||
Status: s.Status,
|
||||
EndpointID: s.EndpointID,
|
||||
DeploymentInfo: s.DeploymentInfo,
|
||||
ReadyRePullImage: s.ReadyRePullImage,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,39 +3,10 @@ package edgestacks
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type aggregatedStatusesMap map[portainer.EdgeStackStatusType]int
|
||||
|
||||
type SummarizedStatus string
|
||||
|
||||
const (
|
||||
sumStatusUnavailable SummarizedStatus = "Unavailable"
|
||||
sumStatusDeploying SummarizedStatus = "Deploying"
|
||||
sumStatusFailed SummarizedStatus = "Failed"
|
||||
sumStatusPaused SummarizedStatus = "Paused"
|
||||
sumStatusPartiallyRunning SummarizedStatus = "PartiallyRunning"
|
||||
sumStatusCompleted SummarizedStatus = "Completed"
|
||||
sumStatusRunning SummarizedStatus = "Running"
|
||||
)
|
||||
|
||||
type edgeStackStatusSummary struct {
|
||||
AggregatedStatus aggregatedStatusesMap
|
||||
Status SummarizedStatus
|
||||
Reason string
|
||||
}
|
||||
|
||||
type edgeStackListResponseItem struct {
|
||||
portainer.EdgeStack
|
||||
StatusSummary edgeStackStatusSummary
|
||||
}
|
||||
|
||||
// @id EdgeStackList
|
||||
// @summary Fetches the list of EdgeStacks
|
||||
// @description **Access policy**: administrator
|
||||
@@ -43,122 +14,16 @@ type edgeStackListResponseItem struct {
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param summarizeStatuses query boolean false "will summarize the statuses"
|
||||
// @success 200 {array} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 503 "Edge compute features are disabled"
|
||||
// @router /edge_stacks [get]
|
||||
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
summarizeStatuses, _ := request.RetrieveBooleanQueryParameter(r, "summarizeStatuses", true)
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge stacks from the database", err)
|
||||
}
|
||||
|
||||
res := make([]edgeStackListResponseItem, len(edgeStacks))
|
||||
|
||||
for i := range edgeStacks {
|
||||
res[i].EdgeStack = edgeStacks[i]
|
||||
|
||||
if summarizeStatuses {
|
||||
if err := fillStatusSummary(handler.DataStore, &res[i]); err != nil {
|
||||
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
|
||||
}
|
||||
} else if err := fillEdgeStackStatus(handler.DataStore, &res[i].EdgeStack); err != nil {
|
||||
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, res)
|
||||
}
|
||||
|
||||
func fillStatusSummary(tx dataservices.DataStoreTx, edgeStack *edgeStackListResponseItem) error {
|
||||
statuses, err := tx.EdgeStackStatus().ReadAll(edgeStack.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aggregated := make(aggregatedStatusesMap)
|
||||
|
||||
for _, envStatus := range statuses {
|
||||
for _, status := range envStatus.Status {
|
||||
aggregated[status.Type]++
|
||||
}
|
||||
}
|
||||
|
||||
status, reason := SummarizeStatuses(statuses, edgeStack.NumDeployments)
|
||||
|
||||
edgeStack.StatusSummary = edgeStackStatusSummary{
|
||||
AggregatedStatus: aggregated,
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
edgeStack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SummarizeStatuses(statuses []portainer.EdgeStackStatusForEnv, numDeployments int) (SummarizedStatus, string) {
|
||||
if numDeployments == 0 {
|
||||
return sumStatusUnavailable, "Your edge stack is currently unavailable due to the absence of an available environment in your edge group"
|
||||
}
|
||||
|
||||
allStatuses := slicesx.FlatMap(statuses, func(x portainer.EdgeStackStatusForEnv) []portainer.EdgeStackDeploymentStatus {
|
||||
return x.Status
|
||||
})
|
||||
|
||||
lastStatuses := slicesx.Map(
|
||||
slicesx.Filter(
|
||||
statuses,
|
||||
func(s portainer.EdgeStackStatusForEnv) bool {
|
||||
return len(s.Status) > 0
|
||||
},
|
||||
),
|
||||
func(x portainer.EdgeStackStatusForEnv) portainer.EdgeStackDeploymentStatus {
|
||||
return x.Status[len(x.Status)-1]
|
||||
},
|
||||
)
|
||||
|
||||
if len(lastStatuses) == 0 {
|
||||
return sumStatusDeploying, ""
|
||||
}
|
||||
|
||||
if allFailed := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
|
||||
return s.Type == portainer.EdgeStackStatusError
|
||||
}); allFailed {
|
||||
return sumStatusFailed, ""
|
||||
}
|
||||
|
||||
if hasPaused := slicesx.Some(allStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
|
||||
return s.Type == portainer.EdgeStackStatusPausedDeploying
|
||||
}); hasPaused {
|
||||
return sumStatusPaused, ""
|
||||
}
|
||||
|
||||
if len(lastStatuses) < numDeployments {
|
||||
return sumStatusDeploying, ""
|
||||
}
|
||||
|
||||
hasDeploying := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusDeploying })
|
||||
hasRunning := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusRunning })
|
||||
hasFailed := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusError })
|
||||
|
||||
if hasRunning && hasFailed && !hasDeploying {
|
||||
return sumStatusPartiallyRunning, ""
|
||||
}
|
||||
|
||||
if allCompleted := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusCompleted }); allCompleted {
|
||||
return sumStatusCompleted, ""
|
||||
}
|
||||
|
||||
if allRunning := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
|
||||
return s.Type == portainer.EdgeStackStatusRunning
|
||||
}); allRunning {
|
||||
return sumStatusRunning, ""
|
||||
}
|
||||
|
||||
return sumStatusDeploying, ""
|
||||
return response.JSON(w, edgeStacks)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EdgeStackStatusDelete
|
||||
// @summary Delete an EdgeStack status
|
||||
// @description Authorized only if the request is done by an Edge Environment(Endpoint)
|
||||
// @tags edge_stacks
|
||||
// @produce json
|
||||
// @param id path int true "EdgeStack Id"
|
||||
// @param environmentId path int true "Environment identifier"
|
||||
// @success 200 {object} portainer.EdgeStack
|
||||
// @failure 500
|
||||
// @failure 400
|
||||
// @failure 404
|
||||
// @failure 403
|
||||
// @deprecated
|
||||
// @router /edge_stacks/{id}/status/{environmentId} [delete]
|
||||
func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid stack identifier route variable", err)
|
||||
}
|
||||
|
||||
endpoint, err := middlewares.FetchEndpoint(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve a valid endpoint from the handler context", err)
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
var stack *portainer.EdgeStack
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
stack, err = handler.deleteEdgeStackStatus(tx, portainer.EdgeStackID(stackID), endpoint)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteEdgeStackStatus(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID, endpoint *portainer.Endpoint) (*portainer.EdgeStack, error) {
|
||||
stack, err := tx.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
return nil, handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
environmentStatus, ok := stack.Status[endpoint.ID]
|
||||
if !ok {
|
||||
environmentStatus = portainer.EdgeStackStatus{}
|
||||
}
|
||||
|
||||
environmentStatus.Status = append(environmentStatus.Status, portainer.EdgeStackDeploymentStatus{
|
||||
Time: time.Now().Unix(),
|
||||
Type: portainer.EdgeStackStatusRemoved,
|
||||
})
|
||||
|
||||
stack.Status[endpoint.ID] = environmentStatus
|
||||
|
||||
err = tx.EdgeStack().UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist the stack changes inside the database", err)
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func TestDeleteStatus(t *testing.T) {
|
||||
handler, _ := setupHandler(t)
|
||||
|
||||
endpoint := createEndpoint(t, handler.DataStore)
|
||||
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d/status/%d", edgeStack.ID, endpoint.ID), nil)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -13,6 +11,8 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type updateStatusPayload struct {
|
||||
@@ -20,7 +20,6 @@ type updateStatusPayload struct {
|
||||
Status *portainer.EdgeStackStatusType
|
||||
EndpointID portainer.EndpointID
|
||||
Time int64
|
||||
Version int
|
||||
}
|
||||
|
||||
func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
@@ -68,33 +67,10 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return httperror.BadRequest("Invalid request payload", fmt.Errorf("edge polling error: %w. Environment ID: %d", err, payload.EndpointID))
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(payload.EndpointID)
|
||||
if err != nil {
|
||||
return handlerDBErr(fmt.Errorf("unable to find the environment from the database: %w. Environment ID: %d", err, payload.EndpointID), "unable to find the environment")
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
}
|
||||
|
||||
var stack *portainer.EdgeStack
|
||||
|
||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
stack, err = tx.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err != nil {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to retrieve Edge stack from the database", err)
|
||||
}
|
||||
|
||||
if err := handler.updateEdgeStackStatus(tx, stack, stack.ID, payload); err != nil {
|
||||
return httperror.InternalServerError("Unable to update Edge stack status", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
stack, err = handler.updateEdgeStackStatus(tx, r, portainer.EdgeStackID(stackID), payload)
|
||||
return err
|
||||
}); err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
@@ -104,49 +80,72 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
if ok, _ := strconv.ParseBool(r.Header.Get("X-Portainer-No-Body")); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fillEdgeStackStatus(handler.DataStore, stack); err != nil {
|
||||
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, stack *portainer.EdgeStack, stackID portainer.EdgeStackID, payload updateStatusPayload) error {
|
||||
if payload.Version > 0 && payload.Version < stack.Version {
|
||||
return nil
|
||||
func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, r *http.Request, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) {
|
||||
stack, err := tx.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
// skip error because agent tries to report on deleted stack
|
||||
log.Debug().
|
||||
Err(err).
|
||||
Int("stackID", int(stackID)).
|
||||
Int("status", int(*payload.Status)).
|
||||
Msg("Unable to find a stack inside the database, skipping error")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to retrieve Edge stack from the database: %w. Environment ID: %d", err, payload.EndpointID)
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(payload.EndpointID)
|
||||
if err != nil {
|
||||
return nil, handler.handlerDBErr(fmt.Errorf("unable to find the environment from the database: %w. Environment ID: %d", err, payload.EndpointID), "unable to find the environment")
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return nil, httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
}
|
||||
|
||||
status := *payload.Status
|
||||
|
||||
log.Debug().
|
||||
Int("stackID", int(stackID)).
|
||||
Int("status", int(status)).
|
||||
Msg("Updating stack status")
|
||||
|
||||
deploymentStatus := portainer.EdgeStackDeploymentStatus{
|
||||
Type: status,
|
||||
Error: payload.Error,
|
||||
Time: payload.Time,
|
||||
}
|
||||
|
||||
if deploymentStatus.Type == portainer.EdgeStackStatusRemoved {
|
||||
return tx.EdgeStackStatus().Delete(stackID, payload.EndpointID)
|
||||
updateEnvStatus(payload.EndpointID, stack, deploymentStatus)
|
||||
|
||||
if err := tx.EdgeStack().UpdateEdgeStack(stackID, stack); err != nil {
|
||||
return nil, handler.handlerDBErr(fmt.Errorf("unable to update Edge stack to the database: %w. Environment name: %s", err, endpoint.Name), "unable to update Edge stack")
|
||||
}
|
||||
|
||||
environmentStatus, err := tx.EdgeStackStatus().Read(stackID, payload.EndpointID)
|
||||
if err != nil && !tx.IsErrObjectNotFound(err) {
|
||||
return err
|
||||
} else if tx.IsErrObjectNotFound(err) {
|
||||
environmentStatus = &portainer.EdgeStackStatusForEnv{
|
||||
EndpointID: payload.EndpointID,
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
func updateEnvStatus(environmentId portainer.EndpointID, stack *portainer.EdgeStack, deploymentStatus portainer.EdgeStackDeploymentStatus) {
|
||||
if deploymentStatus.Type == portainer.EdgeStackStatusRemoved {
|
||||
delete(stack.Status, environmentId)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
environmentStatus, ok := stack.Status[environmentId]
|
||||
if !ok {
|
||||
environmentStatus = portainer.EdgeStackStatus{
|
||||
EndpointID: environmentId,
|
||||
Status: []portainer.EdgeStackDeploymentStatus{},
|
||||
}
|
||||
}
|
||||
|
||||
if containsStatus := slices.ContainsFunc(environmentStatus.Status, func(e portainer.EdgeStackDeploymentStatus) bool {
|
||||
return e.Type == deploymentStatus.Type
|
||||
}); !containsStatus {
|
||||
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
|
||||
}
|
||||
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
|
||||
|
||||
return tx.EdgeStackStatus().Update(stackID, payload.EndpointID, environmentStatus)
|
||||
stack.Status[environmentId] = environmentStatus
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Update Status
|
||||
@@ -29,11 +28,15 @@ func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -45,7 +48,9 @@ func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -57,10 +62,14 @@ func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
|
||||
updatedStack := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
endpointStatus, ok := updatedStack.Status[payload.EndpointID]
|
||||
require.True(t, ok)
|
||||
if !ok {
|
||||
t.Fatal("Missing status")
|
||||
}
|
||||
|
||||
lastStatus := endpointStatus.Status[len(endpointStatus.Status)-1]
|
||||
|
||||
@@ -75,8 +84,8 @@ func TestUpdateStatusAndInspect(t *testing.T) {
|
||||
if endpointStatus.EndpointID != payload.EndpointID {
|
||||
t.Fatalf("expected EndpointID %d, found %d", payload.EndpointID, endpointStatus.EndpointID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
||||
handler, _ := setupHandler(t)
|
||||
|
||||
@@ -127,11 +136,15 @@ func TestUpdateStatusWithInvalidPayload(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Helpers
|
||||
@@ -61,12 +60,14 @@ func setupHandler(t *testing.T) (*Handler, string) {
|
||||
handler.FileService = fs
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
require.NoError(t, err)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings.EnableEdgeComputeFeatures = true
|
||||
|
||||
err = handler.DataStore.Settings().UpdateSettings(settings)
|
||||
require.NoError(t, err)
|
||||
if err := handler.DataStore.Settings().UpdateSettings(settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler.GitService = testhelpers.NewGitService(errors.New("Clone error"), "git-service-id")
|
||||
|
||||
@@ -85,8 +86,9 @@ func createEndpointWithId(t *testing.T, store dataservices.DataStore, endpointID
|
||||
LastCheckInDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
require.NoError(t, err)
|
||||
if err := store.Endpoint().Create(&endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
@@ -107,13 +109,15 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err := store.EdgeGroup().Create(&edgeGroup)
|
||||
require.NoError(t, err)
|
||||
if err := store.EdgeGroup().Create(&edgeGroup); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(14)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
|
||||
ProjectPath: "/project/path",
|
||||
@@ -130,23 +134,13 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
|
||||
},
|
||||
}
|
||||
|
||||
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
|
||||
require.NoError(t, err)
|
||||
if err := store.EdgeStack().Create(edgeStack.ID, &edgeStack); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.EndpointRelation().Create(&endpointRelation)
|
||||
require.NoError(t, err)
|
||||
if err := store.EndpointRelation().Create(&endpointRelation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return edgeStack
|
||||
}
|
||||
|
||||
func createEdgeGroup(t *testing.T, store dataservices.DataStore) portainer.EdgeGroup {
|
||||
edgeGroup := portainer.EdgeGroup{
|
||||
ID: 1,
|
||||
Name: "EdgeGroup 1",
|
||||
}
|
||||
|
||||
err := store.EdgeGroup().Create(&edgeGroup)
|
||||
require.NoError(t, err)
|
||||
|
||||
return edgeGroup
|
||||
}
|
||||
|
||||
@@ -74,17 +74,13 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.InternalServerError("Unexpected error", err)
|
||||
}
|
||||
|
||||
if err := fillEdgeStackStatus(handler.DataStore, stack); err != nil {
|
||||
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID, payload updateEdgeStackPayload) (*portainer.EdgeStack, error) {
|
||||
stack, err := tx.EdgeStack().EdgeStack(stackID)
|
||||
if err != nil {
|
||||
return nil, handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
return nil, handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
relationConfig, err := edge.FetchEndpointRelationsConfig(tx)
|
||||
@@ -111,7 +107,7 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
||||
|
||||
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, payload.DeploymentType)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("unable to check for existence of non fitting environments: %w", err)
|
||||
return nil, httperror.BadRequest("unable to check for existence of non fitting environments: %w", err)
|
||||
}
|
||||
if hasWrongType {
|
||||
return nil, httperror.BadRequest("edge stack with config do not match the environment type", nil)
|
||||
@@ -124,7 +120,7 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
|
||||
stack.EdgeGroups = groupsIds
|
||||
|
||||
if payload.UpdateVersion {
|
||||
if err := handler.updateStackVersion(tx, stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
|
||||
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update stack version", err)
|
||||
}
|
||||
}
|
||||
@@ -142,23 +138,48 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
|
||||
return nil, nil, errors.WithMessage(err, "Unable to retrieve edge stack related environments from database")
|
||||
}
|
||||
|
||||
oldRelatedEnvironmentsSet := set.ToSet(oldRelatedEnvironmentIDs)
|
||||
newRelatedEnvironmentsSet := set.ToSet(newRelatedEnvironmentIDs)
|
||||
oldRelatedSet := set.ToSet(oldRelatedEnvironmentIDs)
|
||||
newRelatedSet := set.ToSet(newRelatedEnvironmentIDs)
|
||||
|
||||
relatedEnvironmentsToAdd := newRelatedEnvironmentsSet.Difference(oldRelatedEnvironmentsSet)
|
||||
relatedEnvironmentsToRemove := oldRelatedEnvironmentsSet.Difference(newRelatedEnvironmentsSet)
|
||||
|
||||
if len(relatedEnvironmentsToRemove) > 0 {
|
||||
if err := tx.EndpointRelation().RemoveEndpointRelationsForEdgeStack(relatedEnvironmentsToRemove.Keys(), edgeStackID); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to remove edge stack relations from the database")
|
||||
endpointsToRemove := set.Set[portainer.EndpointID]{}
|
||||
for endpointID := range oldRelatedSet {
|
||||
if !newRelatedSet[endpointID] {
|
||||
endpointsToRemove[endpointID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(relatedEnvironmentsToAdd) > 0 {
|
||||
if err := tx.EndpointRelation().AddEndpointRelationsForEdgeStack(relatedEnvironmentsToAdd.Keys(), edgeStackID); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to add edge stack relations to the database")
|
||||
for endpointID := range endpointsToRemove {
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to find environment relation in database")
|
||||
}
|
||||
|
||||
delete(relation.EdgeStacks, edgeStackID)
|
||||
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||
}
|
||||
}
|
||||
|
||||
return newRelatedEnvironmentIDs, relatedEnvironmentsToAdd, nil
|
||||
endpointsToAdd := set.Set[portainer.EndpointID]{}
|
||||
for endpointID := range newRelatedSet {
|
||||
if !oldRelatedSet[endpointID] {
|
||||
endpointsToAdd[endpointID] = true
|
||||
}
|
||||
}
|
||||
|
||||
for endpointID := range endpointsToAdd {
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to find environment relation in database")
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStackID] = true
|
||||
|
||||
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "Unable to persist environment relation in database")
|
||||
}
|
||||
}
|
||||
|
||||
return newRelatedEnvironmentIDs, endpointsToAdd, nil
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
endpointID := portainer.EndpointID(6)
|
||||
newEndpoint := createEndpointWithId(t, handler.DataStore, endpointID)
|
||||
|
||||
err := handler.DataStore.Endpoint().Create(&newEndpoint)
|
||||
require.NoError(t, err)
|
||||
if err := handler.DataStore.Endpoint().Create(&newEndpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
endpointRelation := portainer.EndpointRelation{
|
||||
EndpointID: endpointID,
|
||||
@@ -35,8 +36,9 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
|
||||
require.NoError(t, err)
|
||||
if err := handler.DataStore.EndpointRelation().Create(&endpointRelation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newEdgeGroup := portainer.EdgeGroup{
|
||||
ID: 2,
|
||||
@@ -47,8 +49,9 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
PartialMatch: false,
|
||||
}
|
||||
|
||||
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
|
||||
require.NoError(t, err)
|
||||
if err := handler.DataStore.EdgeGroup().Create(&newEdgeGroup); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
payload := updateEdgeStackPayload{
|
||||
StackFileContent: "update-test",
|
||||
@@ -58,11 +61,15 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -74,7 +81,9 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
|
||||
// Get updated edge stack
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -85,8 +94,9 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
updatedStack := portainer.EdgeStack{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
|
||||
require.NoError(t, err)
|
||||
if err := json.NewDecoder(rec.Body).Decode(&updatedStack); err != nil {
|
||||
t.Fatal("error decoding response:", err)
|
||||
}
|
||||
|
||||
if payload.UpdateVersion && updatedStack.Version != edgeStack.Version+1 {
|
||||
t.Fatalf("expected EdgeStack version %d, found %d", edgeStack.Version+1, updatedStack.Version+1)
|
||||
@@ -216,11 +226,15 @@ func TestUpdateWithInvalidPayload(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
jsonPayload, err := json.Marshal(tc.Payload)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
r := bytes.NewBuffer(jsonPayload)
|
||||
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("request error:", err)
|
||||
}
|
||||
|
||||
req.Header.Add("x-api-key", rawAPIKey)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -35,6 +35,8 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
|
||||
h.Handle("/edge_stacks/create/{method}",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(middlewares.Deprecated(h, deprecatedEdgeStackCreateUrlParser)))).Methods(http.MethodPost) // Deprecated
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackList)))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
@@ -51,13 +53,15 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
|
||||
edgeStackStatusRouter := h.NewRoute().Subrouter()
|
||||
edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id"))
|
||||
|
||||
edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func handlerDBErr(err error, msg string) *httperror.HandlerError {
|
||||
func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError {
|
||||
httpErr := httperror.InternalServerError(msg, err)
|
||||
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
httpErr.StatusCode = http.StatusNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -5,18 +5,15 @@ import (
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
edgestackutils "github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (handler *Handler) updateStackVersion(tx dataservices.DataStoreTx, stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte, oldGitHash string, relatedEnvironmentsIDs []portainer.EndpointID) error {
|
||||
stack.Version++
|
||||
|
||||
if err := tx.EdgeStackStatus().Clear(stack.ID, relatedEnvironmentsIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
func (handler *Handler) updateStackVersion(stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte, oldGitHash string, relatedEnvironmentsIDs []portainer.EndpointID) error {
|
||||
stack.Version = stack.Version + 1
|
||||
stack.Status = edgestackutils.NewStatus(stack.Status, relatedEnvironmentsIDs)
|
||||
|
||||
return handler.storeStackFile(stack, deploymentType, config)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package edgetemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
type templateFileFormat struct {
|
||||
Version string `json:"version"`
|
||||
Templates []portainer.Template `json:"templates"`
|
||||
}
|
||||
|
||||
// @id EdgeTemplateList
|
||||
// @deprecated
|
||||
// @summary Fetches the list of Edge Templates
|
||||
// @description **Access policy**: administrator
|
||||
// @tags edge_templates
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @success 200 {array} portainer.Template
|
||||
// @failure 500
|
||||
// @router /edge_templates [get]
|
||||
func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
}
|
||||
|
||||
url := portainer.DefaultTemplatesURL
|
||||
if settings.TemplatesURL != "" {
|
||||
url = settings.TemplatesURL
|
||||
}
|
||||
|
||||
var templateData []byte
|
||||
templateData, err = client.Get(url, 10)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve external templates", err)
|
||||
}
|
||||
|
||||
var templateFile templateFileFormat
|
||||
|
||||
err = json.Unmarshal(templateData, &templateFile)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to parse template file", err)
|
||||
}
|
||||
|
||||
// We only support version 3 of the template format
|
||||
// this is only a temporary fix until we have custom edge templates
|
||||
if templateFile.Version != "3" {
|
||||
return httperror.InternalServerError("Unsupported template version", nil)
|
||||
}
|
||||
|
||||
filteredTemplates := make([]portainer.Template, 0)
|
||||
|
||||
for _, template := range templateFile.Templates {
|
||||
if slices.Contains(template.Categories, "edge") && slices.Contains([]portainer.TemplateType{portainer.ComposeStackTemplate, portainer.SwarmStackTemplate}, template.Type) {
|
||||
filteredTemplates = append(filteredTemplates, template)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, filteredTemplates)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package edgetemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle edge environment(endpoint) operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer security.BouncerService
|
||||
DataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/edge_templates",
|
||||
bouncer.AdminAccess(middlewares.Deprecated(httperror.LoggerHandler(h.edgeTemplateList), func(w http.ResponseWriter, r *http.Request) (string, *httperror.HandlerError) { return "", nil }))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edge"
|
||||
@@ -15,12 +13,8 @@ import (
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var edgeStackSingleFlightGroup = singleflight.Group{}
|
||||
|
||||
// @summary Inspect an Edge Stack for an Environment(Endpoint)
|
||||
// @description **Access policy**: public
|
||||
// @tags edge, endpoints, edge_stacks
|
||||
@@ -48,26 +42,13 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
return httperror.BadRequest("Invalid edge stack identifier route variable", fmt.Errorf("invalid Edge stack route variable: %w. Environment name: %s", err, endpoint.Name))
|
||||
}
|
||||
|
||||
s, err, _ := edgeStackSingleFlightGroup.Do(strconv.Itoa(edgeStackID), func() (any, error) {
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return nil, httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("unable to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
|
||||
}
|
||||
|
||||
return edgeStack, err
|
||||
})
|
||||
if err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("failed to find Edge stack from the database: %w. Environment name: %s", err, endpoint.Name))
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("unable to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("failed to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
|
||||
}
|
||||
|
||||
// WARNING: this variable must not be mutated
|
||||
edgeStack := s.(*portainer.EdgeStack)
|
||||
|
||||
fileName := edgeStack.EntryPoint
|
||||
if endpointutils.IsDockerEndpoint(endpoint) {
|
||||
if fileName == "" {
|
||||
|
||||
@@ -287,8 +287,11 @@ func TestEdgeStackStatus(t *testing.T) {
|
||||
|
||||
edgeStackID := portainer.EdgeStackID(17)
|
||||
edgeStack := portainer.EdgeStack{
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-17",
|
||||
ID: edgeStackID,
|
||||
Name: "test-edge-stack-17",
|
||||
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
|
||||
endpointID: {},
|
||||
},
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||||
ProjectPath: "/project/path",
|
||||
|
||||
@@ -32,9 +32,6 @@ func (handler *Handler) updateEndpointRelations(tx dataservices.DataStoreTx, end
|
||||
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -563,10 +563,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(tx dataservices.Data
|
||||
return err
|
||||
}
|
||||
|
||||
if err := endpointutils.InitializeEdgeEndpointRelation(endpoint, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if err := tx.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) {
|
||||
tag.Endpoints[endpoint.ID] = true
|
||||
|
||||
@@ -91,7 +91,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||
// @failure 403 "Unauthorized access or operation not allowed."
|
||||
// @failure 500 "Server error occurred while attempting to delete the specified environments."
|
||||
// @router /endpoints/delete [post]
|
||||
// @router /endpoints [delete]
|
||||
func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var p endpointDeleteBatchPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &p); err != nil {
|
||||
@@ -127,27 +127,6 @@ func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Reque
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
// @id EndpointDeleteBatchDeprecated
|
||||
// @summary Remove multiple environments
|
||||
// @deprecated
|
||||
// @description Deprecated: use the `POST` endpoint instead.
|
||||
// @description Remove multiple environments and optionally clean-up associated resources.
|
||||
// @description **Access policy**: Administrator only.
|
||||
// @tags endpoints
|
||||
// @security ApiKeyAuth || jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body endpointDeleteBatchPayload true "List of environments to delete, with optional deleteCluster flag to clean-up associated resources (cloud environments only)"
|
||||
// @success 204 "Environment(s) successfully deleted."
|
||||
// @failure 207 {object} endpointDeleteBatchPartialResponse "Partial success. Some environments were deleted successfully, while others failed."
|
||||
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
|
||||
// @failure 403 "Unauthorized access or operation not allowed."
|
||||
// @failure 500 "Server error occurred while attempting to delete the specified environments."
|
||||
// @router /endpoints [delete]
|
||||
func (handler *Handler) endpointDeleteBatchDeprecated(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return handler.endpointDeleteBatch(w, r)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID portainer.EndpointID, deleteCluster bool) error {
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
@@ -214,9 +193,14 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
|
||||
log.Warn().Err(err).Msg("Unable to retrieve edge stacks from the database")
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
if err := tx.EdgeStackStatus().Delete(edgeStack.ID, endpoint.ID); err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to delete edge stack status")
|
||||
for idx := range edgeStacks {
|
||||
edgeStack := &edgeStacks[idx]
|
||||
if _, ok := edgeStack.Status[endpoint.ID]; ok {
|
||||
delete(edgeStack.Status, endpoint.ID)
|
||||
|
||||
if err := tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack); err != nil {
|
||||
log.Warn().Err(err).Msg("Unable to update edge stack")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,13 +80,6 @@ func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
if handler.PullLimitCheckDisabled {
|
||||
return response.JSON(w, &dockerhubStatusResponse{
|
||||
Limit: 10,
|
||||
Remaining: 10,
|
||||
})
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
token, err := getDockerHubToken(httpClient, registry)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,8 +19,6 @@ import (
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param excludeSnapshot query bool false "if true, the snapshot data won't be retrieved"
|
||||
// @param excludeSnapshotRaw query bool false "if true, the SnapshotRaw field won't be retrieved"
|
||||
// @success 200 {object} portainer.Endpoint "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
@@ -39,7 +37,8 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", err)
|
||||
}
|
||||
|
||||
@@ -52,11 +51,9 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(endpoint, settings)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
excludeSnapshot, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshot", true)
|
||||
excludeRaw, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshotRaw", true)
|
||||
|
||||
if !excludeSnapshot {
|
||||
if err := handler.SnapshotService.FillSnapshotData(endpoint, !excludeRaw); err != nil {
|
||||
if !excludeSnapshot(r) {
|
||||
err = handler.SnapshotService.FillSnapshotData(endpoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to add snapshot data", err)
|
||||
}
|
||||
}
|
||||
@@ -86,3 +83,9 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
|
||||
func excludeSnapshot(r *http.Request) bool {
|
||||
excludeSnapshot, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshot", true)
|
||||
|
||||
return excludeSnapshot
|
||||
}
|
||||
|
||||
@@ -38,19 +38,15 @@ const (
|
||||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param excludeIds query []int false "will exclude these environments(endpoints)"
|
||||
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
|
||||
// @param agentVersions query []string false "will return only environments with on of these agent versions"
|
||||
// @param edgeAsync query bool false "if exists true show only edge async agents, false show only standard edge agents. if missing, will show both types (relevant only for edge agents)"
|
||||
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted edge agents, if false show only trusted edge agents (relevant only for edge agents)"
|
||||
// @param edgeCheckInPassedSeconds query number false "if bigger then zero, show only edge agents that checked-in in the last provided seconds (relevant only for edge agents)"
|
||||
// @param excludeSnapshots query bool false "if true, the snapshot data won't be retrieved"
|
||||
// @param excludeSnapshotRaw query bool false "if true, the SnapshotRaw field won't be retrieved"
|
||||
// @param name query string false "will return only environments(endpoints) with this name"
|
||||
// @param edgeStackId query portainer.EdgeStackID false "will return the environements of the specified edge stack"
|
||||
// @param edgeStackStatus query string false "only applied when edgeStackId exists. Filter the returned environments based on their deployment status in the stack (not the environment status!)" Enum("Pending", "Ok", "Error", "Acknowledged", "Remove", "RemoteUpdateSuccess", "ImagesPulled")
|
||||
// @param edgeGroupIds query []int false "List environments(endpoints) of these edge groups"
|
||||
// @param excludeEdgeGroupIds query []int false "Exclude environments(endpoints) of these edge groups"
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints [get]
|
||||
@@ -63,7 +59,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
|
||||
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
|
||||
excludeRaw, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshotRaw", true)
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
|
||||
if err != nil {
|
||||
@@ -95,11 +90,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
return httperror.BadRequest("Invalid query parameters", err)
|
||||
}
|
||||
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(endpoints, query, endpointGroups, edgeGroups, settings, securityContext)
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, edgeGroups, settings)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to filter endpoints", err)
|
||||
}
|
||||
filteredEndpoints = security.FilterEndpoints(filteredEndpoints, endpointGroups, securityContext)
|
||||
|
||||
sortEnvironmentsByField(filteredEndpoints, endpointGroups, getSortKey(sortField), sortOrder == "desc")
|
||||
|
||||
@@ -109,16 +105,14 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
for idx := range paginatedEndpoints {
|
||||
hideFields(&paginatedEndpoints[idx])
|
||||
|
||||
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
if paginatedEndpoints[idx].EdgeCheckinInterval == 0 {
|
||||
paginatedEndpoints[idx].EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings)
|
||||
|
||||
if !query.excludeSnapshots {
|
||||
if err := handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx], !excludeRaw); err != nil {
|
||||
err = handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx])
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to add snapshot data", err)
|
||||
}
|
||||
}
|
||||
@@ -126,7 +120,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
w.Header().Set("X-Total-Available", strconv.Itoa(totalAvailableEndpoints))
|
||||
|
||||
return response.JSON(w, paginatedEndpoints)
|
||||
}
|
||||
|
||||
@@ -137,8 +130,18 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
||||
|
||||
endpointCount := len(endpoints)
|
||||
|
||||
start = min(max(start, 0), endpointCount)
|
||||
end := min(start+limit, endpointCount)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
if start > endpointCount {
|
||||
start = endpointCount
|
||||
}
|
||||
|
||||
end := start + limit
|
||||
if end > endpointCount {
|
||||
end = endpointCount
|
||||
}
|
||||
|
||||
return endpoints[start:end]
|
||||
}
|
||||
@@ -148,10 +151,8 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
|
||||
for _, group := range groups {
|
||||
if group.ID == groupID {
|
||||
endpointGroup = group
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return endpointGroup
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (handler *Handler) listRegistries(tx dataservices.DataStoreTx, r *http.Requ
|
||||
return nil, httperror.InternalServerError("Unable to retrieve registries from the database", err)
|
||||
}
|
||||
|
||||
registries, handleError := handler.filterRegistriesByAccess(tx, r, registries, endpoint, user, securityContext.UserMemberships)
|
||||
registries, handleError := handler.filterRegistriesByAccess(r, registries, endpoint, user, securityContext.UserMemberships)
|
||||
if handleError != nil {
|
||||
return nil, handleError
|
||||
}
|
||||
@@ -87,15 +87,15 @@ func (handler *Handler) listRegistries(tx dataservices.DataStoreTx, r *http.Requ
|
||||
return registries, err
|
||||
}
|
||||
|
||||
func (handler *Handler) filterRegistriesByAccess(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
func (handler *Handler) filterRegistriesByAccess(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
if !endpointutils.IsKubernetesEndpoint(endpoint) {
|
||||
return security.FilterRegistries(registries, user, memberships, endpoint.ID), nil
|
||||
}
|
||||
|
||||
return handler.filterKubernetesEndpointRegistries(tx, r, registries, endpoint, user, memberships)
|
||||
return handler.filterKubernetesEndpointRegistries(r, registries, endpoint, user, memberships)
|
||||
}
|
||||
|
||||
func (handler *Handler) filterKubernetesEndpointRegistries(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
func (handler *Handler) filterKubernetesEndpointRegistries(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
namespaceParam, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
isAdmin, err := security.IsAdmin(r)
|
||||
if err != nil {
|
||||
@@ -116,7 +116,7 @@ func (handler *Handler) filterKubernetesEndpointRegistries(tx dataservices.DataS
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
return handler.filterKubernetesRegistriesByUserRole(tx, r, registries, endpoint, user)
|
||||
return handler.filterKubernetesRegistriesByUserRole(r, registries, endpoint, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) {
|
||||
@@ -169,7 +169,7 @@ func registryAccessPoliciesContainsNamespace(registryAccess portainer.RegistryAc
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) {
|
||||
err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if errors.Is(err, security.ErrAuthorizationRequired) {
|
||||
return nil, httperror.Forbidden("User is not authorized", err)
|
||||
@@ -178,7 +178,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.Dat
|
||||
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
userNamespaces, err := handler.userNamespaces(tx, endpoint, user)
|
||||
userNamespaces, err := handler.userNamespaces(endpoint, user)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("unable to retrieve user namespaces", err)
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.Dat
|
||||
return filterRegistriesByNamespaces(registries, endpoint.ID, userNamespaces), nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userNamespaces(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
|
||||
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
|
||||
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,7 +197,7 @@ func (handler *Handler) userNamespaces(tx dataservices.DataStoreTx, endpoint *po
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userMemberships, err := tx.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||
userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user