Compare commits

..

2 Commits

Author SHA1 Message Date
James Player c4ca049ab8 Bump version to v2.28.1 (#545) 2025-03-20 14:12:55 +13:00
Oscar Zhou 80cc44a17b fix(k8s/config): force insecure-skip-tls-verify option for internal use [BE-11706] (#538) 2025-03-20 08:49:35 +13:00
369 changed files with 2128 additions and 14114 deletions
+11 -10
View File
@@ -91,19 +91,10 @@ 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.29.2'
- '2.29.1'
- '2.29.0'
- '2.28.1'
- '2.28.0'
- '2.27.6'
- '2.27.5'
- '2.27.4'
- '2.27.3'
- '2.27.2'
- '2.27.1'
- '2.27.0'
- '2.26.1'
@@ -120,6 +111,16 @@ body:
- '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'
validations:
required: true
-1
View File
@@ -12,7 +12,6 @@ linters:
- copyloopvar
- intrange
- perfsprint
- ineffassign
linters-settings:
depguard:
+50 -5
View File
@@ -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")
}
-1
View File
@@ -60,7 +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(),
}
}
+16 -17
View File
@@ -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
View File
@@ -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"
)
+45
View File
@@ -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
}
+4 -4
View File
@@ -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{
+7 -9
View File
@@ -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"
@@ -167,8 +166,8 @@ 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) {
@@ -423,7 +422,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))
@@ -577,18 +576,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService,
PlatformService: platformService,
PullLimitCheckDisabled: *flags.PullLimitCheckDisabled,
}
}
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)
-2
View File
@@ -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 {
-26
View File
@@ -244,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
-28
View File
@@ -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 {
-14
View File
@@ -9,7 +9,6 @@ import (
type BaseCRUD[T any, I constraints.Integer] interface {
Create(element *T) error
Read(ID I) (*T, error)
Exists(ID I) (bool, error)
ReadAll() ([]T, error)
Update(ID I, element *T) error
Delete(ID I) error
@@ -43,19 +42,6 @@ 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
}
func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
-6
View File
@@ -28,12 +28,6 @@ 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)
}
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
-8
View File
@@ -93,10 +93,6 @@ func (service ServiceTx) AddEndpointRelationsForEdgeStack(endpointIDs []portaine
}
}
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 {
@@ -123,10 +119,6 @@ func (service ServiceTx) RemoveEndpointRelationsForEdgeStack(endpointIDs []porta
}
}
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 {
-1
View File
@@ -159,7 +159,6 @@ type (
SnapshotService interface {
BaseCRUD[portainer.Snapshot, portainer.EndpointID]
ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error)
}
// SSLSettingsService represents a service for managing application settings
-13
View File
@@ -38,16 +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
}
-23
View File
@@ -12,26 +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
}
+9 -17
View File
@@ -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
}
@@ -610,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.30.1",
"KubectlShellImage": "portainer/kubectl-shell:2.28.1",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -943,7 +943,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.30.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.28.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}
+15
View File
@@ -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)
}
+61
View File
@@ -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
}
+13
View File
@@ -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 {
+6 -5
View File
@@ -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 ""
}
+7 -14
View File
@@ -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
+7 -7
View File
@@ -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)
+2 -2
View File
@@ -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
}
+2 -8
View File
@@ -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
}
+45 -36
View File
@@ -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) {
-173
View File
@@ -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")
}
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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 {
@@ -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) {
@@ -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)
}
@@ -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
+4 -3
View File
@@ -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
+3 -2
View File
@@ -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_.-]")
}
@@ -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")
}
@@ -145,15 +145,11 @@ func (handler *Handler) handleChangeEdgeGroups(tx dataservices.DataStoreTx, edge
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")
}
tx.EndpointRelation().RemoveEndpointRelationsForEdgeStack(relatedEnvironmentsToRemove.Keys(), edgeStackID)
}
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")
}
tx.EndpointRelation().AddEndpointRelationsForEdgeStack(relatedEnvironmentsToAdd.Keys(), edgeStackID)
}
return newRelatedEnvironmentIDs, relatedEnvironmentsToAdd, nil
@@ -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 {
+11 -8
View File
@@ -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
}
+14 -14
View File
@@ -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 {
@@ -110,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)
}
}
@@ -127,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)
}
@@ -138,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]
}
@@ -149,10 +151,8 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
for _, group := range groups {
if group.ID == groupID {
endpointGroup = group
break
}
}
return endpointGroup
}
@@ -272,7 +272,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if err := handler.SnapshotService.FillSnapshotData(endpoint, true); err != nil {
if err := handler.SnapshotService.FillSnapshotData(endpoint); err != nil {
return httperror.InternalServerError("Unable to add snapshot data", err)
}
-86
View File
@@ -37,8 +37,6 @@ type EnvironmentsQuery struct {
edgeStackId portainer.EdgeStackID
edgeStackStatus *portainer.EdgeStackStatusType
excludeIds []portainer.EndpointID
edgeGroupIds []portainer.EdgeGroupID
excludeEdgeGroupIds []portainer.EdgeGroupID
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
@@ -79,16 +77,6 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
return EnvironmentsQuery{}, err
}
edgeGroupIDs, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "edgeGroupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
excludeEdgeGroupIds, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "excludeEdgeGroupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
@@ -129,8 +117,6 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
edgeStackId: portainer.EdgeStackID(edgeStackId),
edgeStackStatus: edgeStackStatus,
edgeGroupIds: edgeGroupIDs,
excludeEdgeGroupIds: excludeEdgeGroupIds,
}, nil
}
@@ -157,14 +143,6 @@ func (handler *Handler) filterEndpointsByQuery(
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
if len(query.edgeGroupIds) > 0 {
filteredEndpoints, edgeGroups = filterEndpointsByEdgeGroupIDs(filteredEndpoints, edgeGroups, query.edgeGroupIds)
}
if len(query.excludeEdgeGroupIds) > 0 {
filteredEndpoints, edgeGroups = filterEndpointsByExcludeEdgeGroupIDs(filteredEndpoints, edgeGroups, query.excludeEdgeGroupIds)
}
if query.name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
}
@@ -317,70 +295,6 @@ func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs
return endpoints[:n]
}
func filterEndpointsByEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeGroupIDs []portainer.EdgeGroupID) ([]portainer.Endpoint, []portainer.EdgeGroup) {
edgeGroupIDFilterSet := make(map[portainer.EdgeGroupID]struct{}, len(edgeGroupIDs))
for _, id := range edgeGroupIDs {
edgeGroupIDFilterSet[id] = struct{}{}
}
n := 0
for _, edgeGroup := range edgeGroups {
if _, exists := edgeGroupIDFilterSet[edgeGroup.ID]; exists {
edgeGroups[n] = edgeGroup
n++
}
}
edgeGroups = edgeGroups[:n]
endpointIDSet := make(map[portainer.EndpointID]struct{})
for _, edgeGroup := range edgeGroups {
for _, endpointID := range edgeGroup.Endpoints {
endpointIDSet[endpointID] = struct{}{}
}
}
n = 0
for _, endpoint := range endpoints {
if _, exists := endpointIDSet[endpoint.ID]; exists {
endpoints[n] = endpoint
n++
}
}
return endpoints[:n], edgeGroups
}
func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []portainer.EdgeGroup, excludeEdgeGroupIds []portainer.EdgeGroupID) ([]portainer.Endpoint, []portainer.EdgeGroup) {
excludeEdgeGroupIDSet := make(map[portainer.EdgeGroupID]struct{}, len(excludeEdgeGroupIds))
for _, id := range excludeEdgeGroupIds {
excludeEdgeGroupIDSet[id] = struct{}{}
}
n := 0
excludeEndpointIDSet := make(map[portainer.EndpointID]struct{})
for _, edgeGroup := range edgeGroups {
if _, ok := excludeEdgeGroupIDSet[edgeGroup.ID]; ok {
for _, endpointID := range edgeGroup.Endpoints {
excludeEndpointIDSet[endpointID] = struct{}{}
}
} else {
edgeGroups[n] = edgeGroup
n++
}
}
edgeGroups = edgeGroups[:n]
n = 0
for _, endpoint := range endpoints {
if _, ok := excludeEndpointIDSet[endpoint.ID]; !ok {
endpoints[n] = endpoint
n++
}
}
return endpoints[:n], edgeGroups
}
func filterEndpointsBySearchCriteria(
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
+13 -14
View File
@@ -26,20 +26,19 @@ func hideFields(endpoint *portainer.Endpoint) {
// Handler is the HTTP handler used to handle environment(endpoint) operations.
type Handler struct {
*mux.Router
requestBouncer security.BouncerService
DataStore dataservices.DataStore
FileService portainer.FileService
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
K8sClientFactory *cli.ClientFactory
ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
DockerClientFactory *dockerclient.ClientFactory
BindAddress string
BindAddressHTTPS string
PendingActionsService *pendingactions.PendingActionsService
PullLimitCheckDisabled bool
requestBouncer security.BouncerService
DataStore dataservices.DataStore
FileService portainer.FileService
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService
K8sClientFactory *cli.ClientFactory
ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
DockerClientFactory *dockerclient.ClientFactory
BindAddress string
BindAddressHTTPS string
PendingActionsService *pendingactions.PendingActionsService
}
// NewHandler creates a handler to manage environment(endpoint) operations.
@@ -9,7 +9,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 fileResponse struct {
@@ -28,7 +29,7 @@ type repositoryFilePreviewPayload struct {
}
func (payload *repositoryFilePreviewPayload) Validate(r *http.Request) error {
if len(payload.Repository) == 0 || !validate.IsURL(payload.Repository) {
if len(payload.Repository) == 0 || !govalidator.IsURL(payload.Repository) {
return errors.New("invalid repository URL. Must correspond to a valid URL format")
}
+1 -1
View File
@@ -81,7 +81,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.30.1
// @version 2.28.1
// @description.markdown api-description.md
// @termsOfService
-12
View File
@@ -54,18 +54,6 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
h.Handle("/{id}/kubernetes/helm",
httperror.LoggerHandler(h.helmInstall)).Methods(http.MethodPost)
// `helm get all [RELEASE_NAME]`
h.Handle("/{id}/kubernetes/helm/{release}",
httperror.LoggerHandler(h.helmGet)).Methods(http.MethodGet)
// `helm history [RELEASE_NAME]`
h.Handle("/{id}/kubernetes/helm/{release}/history",
httperror.LoggerHandler(h.helmGetHistory)).Methods(http.MethodGet)
// `helm rollback [RELEASE_NAME] [REVISION]`
h.Handle("/{id}/kubernetes/helm/{release}/rollback",
httperror.LoggerHandler(h.helmRollback)).Methods(http.MethodPost)
return h
}
+1 -1
View File
@@ -42,7 +42,7 @@ func Test_helmDelete(t *testing.T) {
// Install a single chart directly, to be deleted by the handler
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Upgrade(options)
h.helmPackageManager.Install(options)
t.Run("helmDelete succeeds with admin user", func(t *testing.T) {
req := httptest.NewRequest(http.MethodDelete, "/1/kubernetes/helm/"+options.Name, nil)
-67
View File
@@ -1,67 +0,0 @@
package helm
import (
"net/http"
"github.com/portainer/portainer/pkg/libhelm/options"
_ "github.com/portainer/portainer/pkg/libhelm/release"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id HelmGet
// @summary Get a helm release
// @description Get details of a helm release by release name
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param name path string true "Helm release name"
// @param namespace query string false "specify an optional namespace"
// @param showResources query boolean false "show resources of the release"
// @param revision query int false "specify an optional revision"
// @success 200 {object} release.Release "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the release."
// @router /endpoints/{id}/kubernetes/helm/{name} [get]
func (handler *Handler) helmGet(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
release, err := request.RetrieveRouteVariableValue(r, "release")
if err != nil {
return httperror.BadRequest("No release specified", err)
}
clusterAccess, httperr := handler.getHelmClusterAccess(r)
if httperr != nil {
return httperr
}
// build the get options
getOpts := options.GetOptions{
KubernetesClusterAccess: clusterAccess,
Name: release,
}
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
// optional namespace. The library defaults to "default"
if namespace != "" {
getOpts.Namespace = namespace
}
showResources, _ := request.RetrieveBooleanQueryParameter(r, "showResources", true)
getOpts.ShowResources = showResources
revision, _ := request.RetrieveNumericQueryParameter(r, "revision", true)
// optional revision. The library defaults to the latest revision if not specified
if revision > 0 {
getOpts.Revision = revision
}
releases, err := handler.helmPackageManager.Get(getOpts)
if err != nil {
return httperror.InternalServerError("Helm returned an error", err)
}
return response.JSON(w, releases)
}
-66
View File
@@ -1,66 +0,0 @@
package helm
import (
"io"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
)
func Test_helmGet(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
err := store.Endpoint().Create(&portainer.Endpoint{ID: 1})
is.NoError(err, "Error creating environment")
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "Error creating a user")
jwtService, err := jwt.NewService("1h", store)
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmPackageManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")
// Install a single chart, to be retrieved by the handler
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Upgrade(options)
t.Run("helmGet sucessfuly retrieves helm release", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm/"+options.Name+"?namespace="+options.Namespace, nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
testhelpers.AddTestSecurityCookie(req, "Bearer dummytoken")
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
data := release.Release{}
body, err := io.ReadAll(rr.Body)
is.NoError(err, "ReadAll should not return error")
json.Unmarshal(body, &data)
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
is.Equal("nginx-1", data.Name)
})
}
-58
View File
@@ -1,58 +0,0 @@
package helm
import (
"net/http"
"github.com/portainer/portainer/pkg/libhelm/options"
_ "github.com/portainer/portainer/pkg/libhelm/release"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id HelmGetHistory
// @summary Get a historical list of releases
// @description Get a historical list of releases by release name
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param name path string true "Helm release name"
// @param namespace query string false "specify an optional namespace"
// @success 200 {array} release.Release "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve the historical list of releases."
// @router /endpoints/{id}/kubernetes/helm/{release}/history [get]
func (handler *Handler) helmGetHistory(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
release, err := request.RetrieveRouteVariableValue(r, "release")
if err != nil {
return httperror.BadRequest("No release specified", err)
}
clusterAccess, httperr := handler.getHelmClusterAccess(r)
if httperr != nil {
return httperr
}
historyOptions := options.HistoryOptions{
KubernetesClusterAccess: clusterAccess,
Name: release,
}
// optional namespace. The library defaults to "default"
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
if namespace != "" {
historyOptions.Namespace = namespace
}
releases, err := handler.helmPackageManager.GetHistory(historyOptions)
if err != nil {
return httperror.InternalServerError("Helm returned an error", err)
}
return response.JSON(w, releases)
}
@@ -1,67 +0,0 @@
package helm
import (
"io"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
)
func Test_helmGetHistory(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
err := store.Endpoint().Create(&portainer.Endpoint{ID: 1})
is.NoError(err, "Error creating environment")
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "Error creating a user")
jwtService, err := jwt.NewService("1h", store)
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmPackageManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")
// Install a single chart, to be retrieved by the handler
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Upgrade(options)
t.Run("helmGetHistory sucessfuly retrieves helm release history", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm/"+options.Name+"/history?namespace="+options.Namespace, nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
testhelpers.AddTestSecurityCookie(req, "Bearer dummytoken")
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
data := []release.Release{}
body, err := io.ReadAll(rr.Body)
is.NoError(err, "ReadAll should not return error")
json.Unmarshal(body, &data)
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
is.Equal(1, len(data))
is.Equal("nginx-1", data[0].Name)
})
}
+2 -6
View File
@@ -26,8 +26,6 @@ type installChartPayload struct {
Chart string `json:"chart"`
Repo string `json:"repo"`
Values string `json:"values"`
Version string `json:"version"`
Atomic bool `json:"atomic"`
}
var errChartNameInvalid = errors.New("invalid chart name. " +
@@ -103,10 +101,8 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
installOpts := options.InstallOptions{
Name: p.Name,
Chart: p.Chart,
Version: p.Version,
Namespace: p.Namespace,
Repo: p.Repo,
Atomic: p.Atomic,
KubernetesClusterAccess: clusterAccess,
}
@@ -129,7 +125,7 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
installOpts.ValuesFile = file.Name()
}
release, err := handler.helmPackageManager.Upgrade(installOpts)
release, err := handler.helmPackageManager.Install(installOpts)
if err != nil {
return nil, err
}
@@ -196,7 +192,7 @@ func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte,
g := new(errgroup.Group)
for _, resource := range yamlResources {
g.Go(func() error {
tmpfile, err := os.CreateTemp("", "helm-manifest-*.yaml")
tmpfile, err := os.CreateTemp("", "helm-manifest-*")
if err != nil {
return errors.Wrap(err, "failed to create a tmp helm manifest file")
}
+1 -1
View File
@@ -43,7 +43,7 @@ func Test_helmList(t *testing.T) {
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}
h.helmPackageManager.Upgrade(options)
h.helmPackageManager.Install(options)
t.Run("helmList", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/1/kubernetes/helm", nil)
-105
View File
@@ -1,105 +0,0 @@
package helm
import (
"net/http"
"time"
"github.com/portainer/portainer/pkg/libhelm/options"
_ "github.com/portainer/portainer/pkg/libhelm/release"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
// @id HelmRollback
// @summary Rollback a helm release
// @description Rollback a helm release to a previous revision
// @description **Access policy**: authenticated
// @tags helm
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param release path string true "Helm release name"
// @param namespace query string false "specify an optional namespace"
// @param revision query int false "specify the revision to rollback to (defaults to previous revision if not specified)"
// @param wait query boolean false "wait for resources to be ready (default: false)"
// @param waitForJobs query boolean false "wait for jobs to complete before marking the release as successful (default: false)"
// @param recreate query boolean false "performs pods restart for the resource if applicable (default: true)"
// @param force query boolean false "force resource update through delete/recreate if needed (default: false)"
// @param timeout query int false "time to wait for any individual Kubernetes operation in seconds (default: 300)"
// @success 200 {object} release.Release "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier or release name."
// @failure 500 "Server error occurred while attempting to rollback the release."
// @router /endpoints/{id}/kubernetes/helm/{release}/rollback [post]
func (handler *Handler) helmRollback(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
release, err := request.RetrieveRouteVariableValue(r, "release")
if err != nil {
return httperror.BadRequest("No release specified", err)
}
clusterAccess, httperr := handler.getHelmClusterAccess(r)
if httperr != nil {
return httperr
}
// build the rollback options
rollbackOpts := options.RollbackOptions{
KubernetesClusterAccess: clusterAccess,
Name: release,
// Set default values
Recreate: true, // Default to recreate pods (restart)
Timeout: 5 * time.Minute, // Default timeout of 5 minutes
}
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
// optional namespace. The library defaults to "default"
if namespace != "" {
rollbackOpts.Namespace = namespace
}
revision, _ := request.RetrieveNumericQueryParameter(r, "revision", true)
// optional revision. If not specified, it will rollback to the previous revision
if revision > 0 {
rollbackOpts.Version = revision
}
// Default for wait is false, only set to true if explicitly requested
wait, err := request.RetrieveBooleanQueryParameter(r, "wait", true)
if err == nil {
rollbackOpts.Wait = wait
}
// Default for waitForJobs is false, only set to true if explicitly requested
waitForJobs, err := request.RetrieveBooleanQueryParameter(r, "waitForJobs", true)
if err == nil {
rollbackOpts.WaitForJobs = waitForJobs
}
// Default for recreate is true (set above), override if specified
recreate, err := request.RetrieveBooleanQueryParameter(r, "recreate", true)
if err == nil {
rollbackOpts.Recreate = recreate
}
// Default for force is false, only set to true if explicitly requested
force, err := request.RetrieveBooleanQueryParameter(r, "force", true)
if err == nil {
rollbackOpts.Force = force
}
timeout, _ := request.RetrieveNumericQueryParameter(r, "timeout", true)
// Override default timeout if specified
if timeout > 0 {
rollbackOpts.Timeout = time.Duration(timeout) * time.Second
}
releaseInfo, err := handler.helmPackageManager.Rollback(rollbackOpts)
if err != nil {
return httperror.InternalServerError("Failed to rollback helm release", err)
}
return response.JSON(w, releaseInfo)
}
+3 -1
View File
@@ -146,11 +146,13 @@ func (handler *Handler) getAllKubernetesConfigMaps(r *http.Request) ([]models.K8
}
if isUsed {
err = cli.SetConfigMapsIsUsed(&configMaps)
configMapsWithApplications, err := cli.CombineConfigMapsWithApplications(configMaps)
if err != nil {
log.Error().Err(err).Str("context", "getAllKubernetesConfigMaps").Msg("Unable to combine configMaps with associated applications")
return nil, httperror.InternalServerError("Unable to combine configMaps with associated applications", err)
}
return configMapsWithApplications, nil
}
return configMaps, nil
@@ -36,14 +36,11 @@ func deprecatedNamespaceParser(w http.ResponseWriter, r *http.Request) (string,
// Restore the original body for further use
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return "", httperror.InternalServerError("Unable to read request body", err)
}
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
payload := models.K8sNamespaceDetails{}
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return "", httperror.BadRequest("Invalid request. Unable to parse namespace payload", err)
}
namespaceName := payload.Name
-73
View File
@@ -1,73 +0,0 @@
package kubernetes
import (
"net/http"
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/libkubectl"
"github.com/rs/zerolog/log"
)
type describeResourceResponse struct {
Describe string `json:"describe"`
}
// @id DescribeResource
// @summary Get a description of a kubernetes resource
// @description Get a description of a kubernetes resource.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param name query string true "Resource name"
// @param kind query string true "Resource kind"
// @param namespace query string false "Namespace"
// @success 200 {object} describeResourceResponse "Success"
// @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions."
// @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions."
// @failure 404 "Unable to find an environment with the specified identifier."
// @failure 500 "Server error occurred while attempting to retrieve resource description"
// @router /kubernetes/{id}/describe [get]
func (handler *Handler) describeResource(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
name, err := request.RetrieveQueryParameter(r, "name", false)
if err != nil {
log.Error().Err(err).Str("context", "describeResource").Msg("Invalid query parameter name")
return httperror.BadRequest("an error occurred during the describeResource operation, invalid query parameter name. Error: ", err)
}
kind, err := request.RetrieveQueryParameter(r, "kind", false)
if err != nil {
log.Error().Err(err).Str("context", "describeResource").Msg("Invalid query parameter kind")
return httperror.BadRequest("an error occurred during the describeResource operation, invalid query parameter kind. Error: ", err)
}
namespace, err := request.RetrieveQueryParameter(r, "namespace", true)
if err != nil {
log.Error().Err(err).Str("context", "describeResource").Msg("Invalid query parameter namespace")
return httperror.BadRequest("an error occurred during the describeResource operation, invalid query parameter namespace. Error: ", err)
}
// fetches the token and the correct server URL for the endpoint, similar to getHelmClusterAccess
libKubectlAccess, err := handler.getLibKubectlAccess(r)
if err != nil {
return httperror.InternalServerError("an error occurred during the describeResource operation, failed to get libKubectlAccess. Error: ", err)
}
client, err := libkubectl.NewClient(libKubectlAccess, namespace, "", true)
if err != nil {
log.Error().Err(err).Str("context", "describeResource").Msg("Failed to create kubernetes client")
return httperror.InternalServerError("an error occurred during the describeResource operation, failed to create kubernetes client. Error: ", err)
}
out, err := client.Describe(namespace, name, kind)
if err != nil {
log.Error().Err(err).Str("context", "describeResource").Msg("Failed to describe kubernetes resource")
return httperror.InternalServerError("an error occurred during the describeResource operation, failed to describe kubernetes resource. Error: ", err)
}
return response.JSON(w, describeResourceResponse{Describe: out})
}
-35
View File
@@ -15,7 +15,6 @@ import (
"github.com/portainer/portainer/api/kubernetes/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libkubectl"
"github.com/rs/zerolog/log"
"github.com/gorilla/mux"
@@ -103,7 +102,6 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/cluster_roles/delete", httperror.LoggerHandler(h.deleteClusterRoles)).Methods(http.MethodPost)
endpointRouter.Handle("/cluster_role_bindings", httperror.LoggerHandler(h.getAllKubernetesClusterRoleBindings)).Methods(http.MethodGet)
endpointRouter.Handle("/cluster_role_bindings/delete", httperror.LoggerHandler(h.deleteClusterRoleBindings)).Methods(http.MethodPost)
endpointRouter.Handle("/describe", httperror.LoggerHandler(h.describeResource)).Methods(http.MethodGet)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
@@ -271,36 +269,3 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}
func (handler *Handler) getLibKubectlAccess(r *http.Request) (*libkubectl.ClientAccess, error) {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve user authentication token", err)
}
bearerToken, _, err := handler.JwtService.GenerateToken(tokenData)
if err != nil {
return nil, httperror.Unauthorized("Unauthorized", err)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return nil, httperror.InternalServerError("Unable to find the Kubernetes endpoint associated to the request.", err)
}
sslSettings, err := handler.DataStore.SSLSettings().Settings()
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = r.Host
}
kubeConfigInternal := handler.kubeClusterAccessService.GetClusterDetails(hostURL, endpoint.ID, true)
return &libkubectl.ClientAccess{
Token: bearerToken,
ServerUrl: kubeConfigInternal.ClusterServerURL,
}, nil
}
+3 -1
View File
@@ -130,11 +130,13 @@ func (handler *Handler) getAllKubernetesSecrets(r *http.Request) ([]models.K8sSe
}
if isUsed {
err = cli.SetSecretsIsUsed(&secrets)
secretsWithApplications, err := cli.CombineSecretsWithApplications(secrets)
if err != nil {
log.Error().Err(err).Str("context", "GetAllKubernetesSecrets").Msg("Unable to combine secrets with associated applications")
return nil, httperror.InternalServerError("unable to combine secrets with associated applications. Error: ", err)
}
return secretsWithApplications, nil
}
return secrets, nil
-8
View File
@@ -7,9 +7,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/pkg/libcrypto"
libclient "github.com/portainer/portainer/pkg/libhttp/client"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -39,12 +37,6 @@ type motdData struct {
// @success 200 {object} motdResponse
// @router /motd [get]
func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
if err := libclient.ExternalRequestDisabled(portainer.MessageOfTheDayURL); err != nil {
log.Debug().Err(err).Msg("External request disabled: MOTD")
response.JSON(w, &motdResponse{Message: ""})
return
}
motd, err := client.Get(portainer.MessageOfTheDayURL, 0)
if err != nil {
response.JSON(w, &motdResponse{Message: ""})
+4 -4
View File
@@ -14,8 +14,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/pkg/errors"
"golang.org/x/oauth2"
)
@@ -62,15 +62,15 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
}
if payload.LogoURL != nil && *payload.LogoURL != "" && !validate.IsURL(*payload.LogoURL) {
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
return errors.New("Invalid logo URL. Must correspond to a valid URL format")
}
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !validate.IsURL(*payload.TemplatesURL) {
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !validate.IsURL(*payload.HelmRepositoryURL) {
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
}
@@ -14,8 +14,8 @@ import (
"github.com/portainer/portainer/api/stacks/stackutils"
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"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -205,7 +205,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
if len(payload.Name) == 0 {
return errors.New("Invalid stack name")
}
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.RepositoryPassword) == 0 {
@@ -15,8 +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"
"github.com/pkg/errors"
)
@@ -96,7 +96,7 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
}
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
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")
}
@@ -112,7 +112,7 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
}
func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error {
if len(payload.ManifestURL) == 0 || !validate.IsURL(payload.ManifestURL) {
if len(payload.ManifestURL) == 0 || !govalidator.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL")
}
@@ -11,8 +11,8 @@ import (
"github.com/portainer/portainer/api/stacks/stackutils"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
valid "github.com/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
)
@@ -142,7 +142,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
if len(payload.SwarmID) == 0 {
return errors.New("Invalid Swarm ID")
}
if len(payload.RepositoryURL) == 0 || !valid.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.RepositoryPassword) == 0 {
+1 -1
View File
@@ -33,7 +33,7 @@ func (handler *Handler) systemNodesCount(w http.ResponseWriter, r *http.Request)
var nodes int
for _, endpoint := range endpoints {
if err := snapshot.FillSnapshotData(handler.dataStore, &endpoint, false); err != nil {
if err := snapshot.FillSnapshotData(handler.dataStore, &endpoint); err != nil {
return httperror.InternalServerError("Unable to add snapshot data", err)
}
+1 -6
View File
@@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/pkg/build"
libclient "github.com/portainer/portainer/pkg/libhttp/client"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/response"
@@ -70,14 +69,10 @@ func (handler *Handler) version(w http.ResponseWriter, r *http.Request) *httperr
}
func GetLatestVersion() string {
if err := libclient.ExternalRequestDisabled(portainer.VersionCheckURL); err != nil {
log.Debug().Err(err).Msg("External request disabled: Version check")
return ""
}
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
log.Debug().Err(err).Msg("couldn't fetch latest Portainer release version")
return ""
}
@@ -45,6 +45,7 @@ func (payload *teamMembershipCreatePayload) Validate(r *http.Request) error {
// @produce json
// @param body body teamMembershipCreatePayload true "Team membership details"
// @success 200 {object} portainer.TeamMembership "Success"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied to manage memberships"
// @failure 409 "Team membership already registered"
+1
View File
@@ -21,6 +21,7 @@ import (
// @produce json
// @param id path int true "Team identifier"
// @success 200 {object} portainer.Team "Success"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Team not found"
+1
View File
@@ -30,6 +30,7 @@ func (payload *teamUpdatePayload) Validate(r *http.Request) error {
// @param id path int true "Team identifier"
// @param body body teamUpdatePayload true "Team details"
// @success 200 {object} portainer.Team "Success"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Team not found"
@@ -4,9 +4,7 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
libclient "github.com/portainer/portainer/pkg/libhttp/client"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -26,20 +24,13 @@ func (handler *Handler) fetchTemplates() (*listResponse, *httperror.HandlerError
templatesURL = portainer.DefaultTemplatesURL
}
var body *listResponse
if err := libclient.ExternalRequestDisabled(templatesURL); err != nil {
if templatesURL == portainer.DefaultTemplatesURL {
log.Debug().Err(err).Msg("External request disabled: Default templates")
return body, nil
}
}
resp, err := http.Get(templatesURL)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve templates via the network", err)
}
defer resp.Body.Close()
var body *listResponse
err = json.NewDecoder(resp.Body).Decode(&body)
if err != nil {
return nil, httperror.InternalServerError("Unable to parse template file", err)
+4 -3
View File
@@ -52,12 +52,12 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
teamLeaderRouter := h.NewRoute().Subrouter()
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
authenticatedRouter := h.NewRoute().Subrouter()
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
restrictedRouter := h.NewRoute().Subrouter()
restrictedRouter.Use(bouncer.RestrictedAccess)
authenticatedRouter := h.NewRoute().Subrouter()
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
publicRouter := h.NewRoute().Subrouter()
publicRouter.Use(bouncer.PublicAccess)
@@ -65,6 +65,7 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
restrictedRouter.Handle("/users", httperror.LoggerHandler(h.userList)).Methods(http.MethodGet)
authenticatedRouter.Handle("/users/me", httperror.LoggerHandler(h.userInspectMe)).Methods(http.MethodGet)
restrictedRouter.Handle("/users/me", httperror.LoggerHandler(h.userInspectMe)).Methods(http.MethodGet)
restrictedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userInspect)).Methods(http.MethodGet)
authenticatedRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userUpdate)).Methods(http.MethodPut)
adminRouter.Handle("/users/{id}", httperror.LoggerHandler(h.userDelete)).Methods(http.MethodDelete)
@@ -11,7 +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/portainer/portainer/pkg/validate"
"github.com/asaskevich/govalidator"
)
type userAccessTokenCreatePayload struct {
@@ -23,10 +24,10 @@ func (payload *userAccessTokenCreatePayload) Validate(r *http.Request) error {
if len(payload.Description) == 0 {
return errors.New("invalid description: cannot be empty")
}
if validate.HasWhitespaceOnly(payload.Description) {
if govalidator.HasWhitespaceOnly(payload.Description) {
return errors.New("invalid description: cannot contain only whitespaces")
}
if validate.MinStringLength(payload.Description, 128) {
if govalidator.MinStringLength(payload.Description, "128") {
return errors.New("invalid description: cannot be longer than 128 characters")
}
return nil
@@ -49,7 +50,7 @@ type accessTokenResponse struct {
// @produce json
// @param id path int true "User identifier"
// @param body body userAccessTokenCreatePayload true "details"
// @success 200 {object} accessTokenResponse "Success"
// @success 200 {object} accessTokenResponse "Created"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
@@ -114,7 +115,7 @@ func (handler *Handler) userCreateAccessToken(w http.ResponseWriter, r *http.Req
return httperror.InternalServerError("Internal Server Error", err)
}
return response.JSONWithStatus(w, accessTokenResponse{rawAPIKey, *apiKey}, http.StatusOK)
return response.JSONWithStatus(w, accessTokenResponse{rawAPIKey, *apiKey}, http.StatusCreated)
}
func (handler *Handler) usesInternalAuthentication(userid portainer.UserID) (bool, error) {
@@ -60,7 +60,7 @@ func Test_userCreateAccessToken(t *testing.T) {
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
is.Equal(http.StatusOK, rr.Code)
is.Equal(http.StatusCreated, rr.Code)
body, err := io.ReadAll(rr.Body)
is.NoError(err, "ReadAll should not return error")
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"github.com/portainer/portainer/api/ws"
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"
"github.com/gorilla/websocket"
)
@@ -38,7 +38,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
if err != nil {
return httperror.BadRequest("Invalid query parameter: id", err)
}
if !validate.IsHexadecimal(attachID) {
if !govalidator.IsHexadecimal(attachID) {
return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err)
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"github.com/portainer/portainer/api/ws"
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"
"github.com/gorilla/websocket"
"github.com/segmentio/encoding/json"
)
@@ -42,7 +42,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
if err != nil {
return httperror.BadRequest("Invalid query parameter: id", err)
}
if !validate.IsHexadecimal(execID) {
if !govalidator.IsHexadecimal(execID) {
return httperror.BadRequest("Invalid query parameter: id (must be hexadecimal identifier)", err)
}
-25
View File
@@ -1,25 +0,0 @@
package middlewares
import (
"net/http"
"runtime/debug"
"github.com/rs/zerolog/log"
)
func WithPanicLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Error().
Any("panic", err).
Str("method", req.Method).
Str("url", req.URL.String()).
Str("stack", string(debug.Stack())).
Msg("Panic in request handler")
}
}()
next.ServeHTTP(w, req)
})
}
@@ -1,36 +0,0 @@
package middlewares
import (
"net/http"
"slices"
"github.com/gorilla/csrf"
)
var (
// Idempotent (safe) methods as defined by RFC7231 section 4.2.2.
safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
)
type plainTextHTTPRequestHandler struct {
next http.Handler
}
func (h *plainTextHTTPRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if slices.Contains(safeMethods, r.Method) {
h.next.ServeHTTP(w, r)
return
}
req := r
// If original request was HTTPS (via proxy), keep CSRF checks.
if xfproto := r.Header.Get("X-Forwarded-Proto"); xfproto != "https" {
req = csrf.PlaintextHTTPRequest(r)
}
h.next.ServeHTTP(w, req)
}
func PlaintextHTTPRequest(next http.Handler) http.Handler {
return &plainTextHTTPRequestHandler{next: next}
}
+1 -1
View File
@@ -45,7 +45,7 @@ func WithItem[TId ~int, TObject any](getter ItemGetter[TId, TObject], idParam st
}
}
func FetchItem[T any](request *http.Request, contextKey ItemContextKey) (*T, error) {
func FetchItem[T any](request *http.Request, contextKey string) (*T, error) {
contextData := request.Context().Value(contextKey)
if contextData == nil {
return nil, errors.New("unable to find item in request context")
+3 -2
View File
@@ -52,7 +52,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
endpointURL.Scheme = "https"
}
proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = agent.NewTransport(factory.signatureService, httpTransport)
@@ -63,7 +63,8 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
Port: 0,
}
if err := proxyServer.start(); err != nil {
err = proxyServer.start()
if err != nil {
return nil, errors.Wrap(err, "failed starting proxy server")
}
+1 -1
View File
@@ -15,7 +15,7 @@ func newAzureProxy(endpoint *portainer.Endpoint, dataStore dataservices.DataStor
return nil, err
}
proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials, dataStore, endpoint)
return proxy, nil
}
+1 -1
View File
@@ -72,7 +72,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
return nil, err
}
proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = dockerTransport
return proxy, nil
}
+1 -1
View File
@@ -224,7 +224,7 @@ func (transport *Transport) getDockerID() (string, error) {
if transport.snapshotService != nil {
endpoint := portainer.Endpoint{ID: transport.endpoint.ID}
if err := transport.snapshotService.FillSnapshotData(&endpoint, true); err == nil && len(endpoint.Snapshots) > 0 {
if err := transport.snapshotService.FillSnapshotData(&endpoint); err == nil && len(endpoint.Snapshots) > 0 {
if dockerID, err := snapshot.FetchDockerID(endpoint.Snapshots[0]); err == nil {
transport.dockerID = dockerID
return dockerID, nil
+1 -1
View File
@@ -13,7 +13,7 @@ func newGitlabProxy(uri string) (http.Handler, error) {
return nil, err
}
proxy := NewSingleHostReverseProxyWithHostHeader(url)
proxy := newSingleHostReverseProxyWithHostHeader(url)
proxy.Transport = gitlab.NewTransport()
return proxy, nil
}
+3 -3
View File
@@ -43,7 +43,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
return nil, err
}
proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = transport
return proxy, nil
@@ -73,7 +73,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
}
endpointURL.Scheme = "http"
proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.signatureService, factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory)
return proxy, nil
@@ -104,7 +104,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
return nil, err
}
proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
return proxy, nil
+3 -11
View File
@@ -10,14 +10,9 @@ import (
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
return &httputil.ReverseProxy{Director: createDirector(target)}
}
func createDirector(target *url.URL) func(*http.Request) {
sensitiveHeaders := []string{"Cookie", "X-Csrf-Token"}
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
targetQuery := target.RawQuery
return func(req *http.Request) {
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
@@ -31,11 +26,8 @@ func createDirector(target *url.URL) func(*http.Request) {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}
for _, header := range sensitiveHeaders {
delete(req.Header, header)
}
}
return &httputil.ReverseProxy{Director: director}
}
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
@@ -1,116 +0,0 @@
package factory
import (
"net/http"
"net/url"
"testing"
"github.com/google/go-cmp/cmp"
)
func Test_createDirector(t *testing.T) {
testCases := []struct {
name string
target *url.URL
req *http.Request
expectedReq *http.Request
}{
{
name: "base case",
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
req: createRequest(
t,
"GET",
"https://agent-portainer.io/test?c=7",
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": "something"},
),
expectedReq: createRequest(
t,
"GET",
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": "something"},
),
},
{
name: "no User-Agent",
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
req: createRequest(
t,
"GET",
"https://agent-portainer.io/test?c=7",
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json"},
),
expectedReq: createRequest(
t,
"GET",
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": ""},
),
},
{
name: "Sensitive Headers",
target: createURL(t, "https://portainer.io/api/docker?a=5&b=6"),
req: createRequest(
t,
"GET",
"https://agent-portainer.io/test?c=7",
map[string]string{
"Accept-Encoding": "gzip",
"Accept": "application/json",
"User-Agent": "something",
"Cookie": "junk",
"X-Csrf-Token": "junk",
},
),
expectedReq: createRequest(
t,
"GET",
"https://portainer.io/api/docker/test?a=5&b=6&c=7",
map[string]string{"Accept-Encoding": "gzip", "Accept": "application/json", "User-Agent": "something"},
),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
director := createDirector(tc.target)
director(tc.req)
if diff := cmp.Diff(tc.req, tc.expectedReq, cmp.Comparer(compareRequests)); diff != "" {
t.Fatalf("requests are different: \n%s", diff)
}
})
}
}
func createURL(t *testing.T, urlString string) *url.URL {
parsedURL, err := url.Parse(urlString)
if err != nil {
t.Fatalf("Failed to create url: %s", err)
}
return parsedURL
}
func createRequest(t *testing.T, method, url string, headers map[string]string) *http.Request {
req, err := http.NewRequest(method, url, nil)
if err != nil {
t.Fatalf("Failed to create http request: %s", err)
} else {
for k, v := range headers {
req.Header.Add(k, v)
}
}
return req
}
func compareRequests(a, b *http.Request) bool {
methodEqual := a.Method == b.Method
urlEqual := cmp.Diff(a.URL, b.URL) == ""
hostEqual := a.Host == b.Host
protoEqual := a.Proto == b.Proto && a.ProtoMajor == b.ProtoMajor && a.ProtoMinor == b.ProtoMinor
headersEqual := cmp.Diff(a.Header, b.Header) == ""
return methodEqual && urlEqual && hostEqual && protoEqual && headersEqual
}
+5 -3
View File
@@ -243,7 +243,8 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
return
}
if ok, err := bouncer.dataStore.User().Exists(tokenData.ID); !ok {
_, err = bouncer.dataStore.User().Read(tokenData.ID)
if bouncer.dataStore.IsErrObjectNotFound(err) {
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
return
} else if err != nil {
@@ -321,8 +322,9 @@ func (bouncer *RequestBouncer) mwAuthenticateFirst(tokenLookups []tokenLookup, n
return
}
if ok, _ := bouncer.dataStore.User().Exists(token.ID); !ok {
httperror.WriteError(w, http.StatusUnauthorized, "The authorization token is invalid", httperrors.ErrUnauthorized)
user, _ := bouncer.dataStore.User().Read(token.ID)
if user == nil {
httperror.WriteError(w, http.StatusUnauthorized, "An authorization token is invalid", httperrors.ErrUnauthorized)
return
}
-3
View File
@@ -344,7 +344,6 @@ func Test_apiKeyLookup(t *testing.T) {
req.Header.Add("x-api-key", rawAPIKey)
token, err := bouncer.apiKeyLookup(req)
require.NoError(t, err)
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
is.Equal(expectedToken, token)
@@ -359,7 +358,6 @@ func Test_apiKeyLookup(t *testing.T) {
req.Header.Add("x-api-key", rawAPIKey)
token, err := bouncer.apiKeyLookup(req)
require.NoError(t, err)
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
is.Equal(expectedToken, token)
@@ -374,7 +372,6 @@ func Test_apiKeyLookup(t *testing.T) {
req.Header.Add("x-api-key", rawAPIKey)
token, err := bouncer.apiKeyLookup(req)
require.NoError(t, err)
expectedToken := &portainer.TokenData{ID: user.ID, Username: user.Username, Role: portainer.StandardUserRole}
is.Equal(expectedToken, token)
+2 -4
View File
@@ -112,7 +112,6 @@ type Server struct {
AdminCreationDone chan struct{}
PendingActionsService *pendingactions.PendingActionsService
PlatformService platform.Service
PullLimitCheckDisabled bool
}
// Start starts the HTTP server
@@ -182,7 +181,6 @@ func (server *Server) Start() error {
endpointHandler.BindAddress = server.BindAddress
endpointHandler.BindAddressHTTPS = server.BindAddressHTTPS
endpointHandler.PendingActionsService = server.PendingActionsService
endpointHandler.PullLimitCheckDisabled = server.PullLimitCheckDisabled
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer, server.DataStore, server.FileService, server.ReverseTunnelService)
@@ -337,7 +335,7 @@ func (server *Server) Start() error {
handler := adminMonitor.WithRedirect(offlineGate.WaitingMiddleware(time.Minute, server.Handler))
handler = middlewares.WithPanicLogger(middlewares.WithSlowRequestsLogger(handler))
handler = middlewares.WithSlowRequestsLogger(handler)
handler, err := csrf.WithProtect(handler)
if err != nil {
@@ -349,7 +347,7 @@ func (server *Server) Start() error {
log.Info().Str("bind_address", server.BindAddress).Msg("starting HTTP server")
httpServer := &http.Server{
Addr: server.BindAddress,
Handler: middlewares.PlaintextHTTPRequest(handler),
Handler: handler,
ErrorLog: errorLogger,
}
+17 -11
View File
@@ -1,8 +1,6 @@
package edge
import (
"slices"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
@@ -15,19 +13,21 @@ func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []porta
return edgeGroup.Endpoints
}
endpointGroupsMap := map[portainer.EndpointGroupID]*portainer.EndpointGroup{}
for i, group := range endpointGroups {
endpointGroupsMap[group.ID] = &endpointGroups[i]
}
endpointIDs := []portainer.EndpointID{}
for _, endpoint := range endpoints {
if !endpointutils.IsEdgeEndpoint(&endpoint) {
continue
}
endpointGroup := endpointGroupsMap[endpoint.GroupID]
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, endpointGroup) {
var endpointGroup portainer.EndpointGroup
for _, group := range endpointGroups {
if endpoint.GroupID == group.ID {
endpointGroup = group
break
}
}
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
@@ -72,11 +72,17 @@ func GetEndpointsFromEdgeGroups(edgeGroupIDs []portainer.EdgeGroupID, datastore
// edgeGroupRelatedToEndpoint returns true if edgeGroup is associated with environment(endpoint)
func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) bool {
if !edgeGroup.Dynamic {
return slices.Contains(edgeGroup.Endpoints, endpoint.ID)
for _, endpointID := range edgeGroup.Endpoints {
if endpoint.ID == endpointID {
return true
}
}
return false
}
endpointTags := tag.Set(endpoint.TagIDs)
if endpointGroup != nil && endpointGroup.TagIDs != nil {
if endpointGroup.TagIDs != nil {
endpointTags = tag.Union(endpointTags, tag.Set(endpointGroup.TagIDs))
}
+4 -12
View File
@@ -170,8 +170,8 @@ func (service *Service) Create(snapshot portainer.Snapshot) error {
return service.dataStore.Snapshot().Create(&snapshot)
}
func (service *Service) FillSnapshotData(endpoint *portainer.Endpoint, includeRaw bool) error {
return FillSnapshotData(service.dataStore, endpoint, includeRaw)
func (service *Service) FillSnapshotData(endpoint *portainer.Endpoint) error {
return FillSnapshotData(service.dataStore, endpoint)
}
func (service *Service) snapshotKubernetesEndpoint(endpoint *portainer.Endpoint) error {
@@ -328,16 +328,8 @@ func FetchDockerID(snapshot portainer.DockerSnapshot) (string, error) {
return info.Swarm.Cluster.ID, nil
}
func FillSnapshotData(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, includeRaw bool) error {
var snapshot *portainer.Snapshot
var err error
if includeRaw {
snapshot, err = tx.Snapshot().Read(endpoint.ID)
} else {
snapshot, err = tx.Snapshot().ReadWithoutSnapshotRaw(endpoint.ID)
}
func FillSnapshotData(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint) error {
snapshot, err := tx.Snapshot().Read(endpoint.ID)
if tx.IsErrObjectNotFound(err) {
endpoint.Snapshots = []portainer.DockerSnapshot{}
endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{}
+49 -23
View File
@@ -110,11 +110,9 @@ type datastoreOption = func(d *testDatastore)
func NewDatastore(options ...datastoreOption) *testDatastore {
conn, _ := database.NewDatabase("boltdb", "", nil)
d := testDatastore{connection: conn}
for _, o := range options {
o(&d)
}
return &d
}
@@ -130,7 +128,6 @@ func (s *stubSettingsService) Settings() (*portainer.Settings, error) {
func (s *stubSettingsService) UpdateSettings(settings *portainer.Settings) error {
s.settings = settings
return nil
}
@@ -143,16 +140,19 @@ func WithSettingsService(settings *portainer.Settings) datastoreOption {
}
type stubUserService struct {
dataservices.UserService
users []portainer.User
}
func (s *stubUserService) BucketName() string { return "users" }
func (s *stubUserService) ReadAll() ([]portainer.User, error) { return s.users, nil }
func (s *stubUserService) BucketName() string { return "users" }
func (s *stubUserService) Read(ID portainer.UserID) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) UserByUsername(username string) (*portainer.User, error) { return nil, nil }
func (s *stubUserService) ReadAll() ([]portainer.User, error) { return s.users, nil }
func (s *stubUserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
return s.users, nil
}
func (s *stubUserService) Create(user *portainer.User) error { return nil }
func (s *stubUserService) Update(ID portainer.UserID, user *portainer.User) error { return nil }
func (s *stubUserService) Delete(ID portainer.UserID) error { return nil }
// WithUsers testDatastore option that will instruct testDatastore to return provided users
func WithUsers(us []portainer.User) datastoreOption {
@@ -162,13 +162,32 @@ func WithUsers(us []portainer.User) datastoreOption {
}
type stubEdgeJobService struct {
dataservices.EdgeJobService
jobs []portainer.EdgeJob
}
func (s *stubEdgeJobService) BucketName() string { return "edgejobs" }
func (s *stubEdgeJobService) ReadAll() ([]portainer.EdgeJob, error) { return s.jobs, nil }
func (s *stubEdgeJobService) Read(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
return nil, nil
}
func (s *stubEdgeJobService) Create(edgeJob *portainer.EdgeJob) error {
return nil
}
func (s *stubEdgeJobService) CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
return nil
}
func (s *stubEdgeJobService) Update(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
return nil
}
func (s *stubEdgeJobService) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
return nil
}
func (s *stubEdgeJobService) Delete(ID portainer.EdgeJobID) error { return nil }
func (s *stubEdgeJobService) GetNextIdentifier() int { return 0 }
// WithEdgeJobs option will instruct testDatastore to return provided jobs
func WithEdgeJobs(js []portainer.EdgeJob) datastoreOption {
@@ -178,8 +197,6 @@ func WithEdgeJobs(js []portainer.EdgeJob) datastoreOption {
}
type stubEndpointRelationService struct {
dataservices.EndpointRelationService
relations []portainer.EndpointRelation
}
@@ -198,6 +215,10 @@ func (s *stubEndpointRelationService) EndpointRelation(ID portainer.EndpointID)
return nil, errors.ErrObjectNotFound
}
func (s *stubEndpointRelationService) Create(EndpointRelation *portainer.EndpointRelation) error {
return nil
}
func (s *stubEndpointRelationService) UpdateEndpointRelation(ID portainer.EndpointID, relation *portainer.EndpointRelation) error {
for i, r := range s.relations {
if r.EndpointID == ID {
@@ -232,6 +253,11 @@ func (s *stubEndpointRelationService) RemoveEndpointRelationsForEdgeStack(endpoi
return nil
}
func (s *stubEndpointRelationService) DeleteEndpointRelation(ID portainer.EndpointID) error {
return nil
}
func (s *stubEndpointRelationService) GetNextIdentifier() int { return 0 }
// WithEndpointRelations option will instruct testDatastore to return provided jobs
func WithEndpointRelations(relations []portainer.EndpointRelation) datastoreOption {
return func(d *testDatastore) {
@@ -330,7 +356,6 @@ func (s *stubEndpointService) EndpointsByTeamID(teamID portainer.TeamID) ([]port
}
}
}
return endpoints, nil
}
@@ -342,19 +367,29 @@ func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
}
type stubStacksService struct {
dataservices.StackService
stacks []portainer.Stack
}
func (s *stubStacksService) BucketName() string { return "stacks" }
func (s *stubStacksService) Create(stack *portainer.Stack) error {
return nil
}
func (s *stubStacksService) Update(ID portainer.StackID, stack *portainer.Stack) error {
return nil
}
func (s *stubStacksService) Delete(ID portainer.StackID) error {
return nil
}
func (s *stubStacksService) Read(ID portainer.StackID) (*portainer.Stack, error) {
for _, stack := range s.stacks {
if stack.ID == ID {
return &stack, nil
}
}
return nil, errors.ErrObjectNotFound
}
@@ -370,7 +405,6 @@ func (s *stubStacksService) StacksByEndpointID(endpointID portainer.EndpointID)
result = append(result, stack)
}
}
return result, nil
}
@@ -382,7 +416,6 @@ func (s *stubStacksService) RefreshableStacks() ([]portainer.Stack, error) {
result = append(result, stack)
}
}
return result, nil
}
@@ -392,7 +425,6 @@ func (s *stubStacksService) StackByName(name string) (*portainer.Stack, error) {
return &stack, nil
}
}
return nil, errors.ErrObjectNotFound
}
@@ -404,7 +436,6 @@ func (s *stubStacksService) StacksByName(name string) ([]portainer.Stack, error)
result = append(result, stack)
}
}
return result, nil
}
@@ -414,7 +445,6 @@ func (s *stubStacksService) StackByWebhookID(webhookID string) (*portainer.Stack
return &stack, nil
}
}
return nil, errors.ErrObjectNotFound
}
@@ -422,10 +452,6 @@ func (s *stubStacksService) GetNextIdentifier() int {
return len(s.stacks)
}
func (s *stubStacksService) Exists(ID portainer.StackID) (bool, error) {
return false, nil
}
// WithStacks option will instruct testDatastore to return provided stacks
func WithStacks(stacks []portainer.Stack) datastoreOption {
return func(d *testDatastore) {
+70 -30
View File
@@ -153,6 +153,46 @@ func (kcl *KubeClient) GetApplicationsResource(namespace, node string) (models.K
return resource, nil
}
// GetApplicationsFromConfigMap gets a list of applications that use a specific ConfigMap
// by checking all pods in the same namespace as the ConfigMap
func (kcl *KubeClient) GetApplicationNamesFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
applications := []string{}
for _, pod := range pods {
if pod.Namespace == configMap.Namespace {
if isPodUsingConfigMap(&pod, configMap.Name) {
application, err := kcl.ConvertPodToApplication(pod, PortainerApplicationResources{
ReplicaSets: replicaSets,
}, false)
if err != nil {
return nil, err
}
applications = append(applications, application.Name)
}
}
}
return applications, nil
}
func (kcl *KubeClient) GetApplicationNamesFromSecret(secret models.K8sSecret, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]string, error) {
applications := []string{}
for _, pod := range pods {
if pod.Namespace == secret.Namespace {
if isPodUsingSecret(&pod, secret.Name) {
application, err := kcl.ConvertPodToApplication(pod, PortainerApplicationResources{
ReplicaSets: replicaSets,
}, false)
if err != nil {
return nil, err
}
applications = append(applications, application.Name)
}
}
}
return applications, nil
}
// ConvertPodToApplication converts a pod to an application, updating owner references if necessary
func (kcl *KubeClient) ConvertPodToApplication(pod corev1.Pod, portainerApplicationResources PortainerApplicationResources, withResource bool) (*models.K8sApplication, error) {
if isReplicaSetOwner(pod) {
@@ -433,23 +473,23 @@ func (kcl *KubeClient) GetApplicationFromServiceSelector(pods []corev1.Pod, serv
func (kcl *KubeClient) GetApplicationConfigurationOwnersFromConfigMap(configMap models.K8sConfigMap, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]models.K8sConfigurationOwnerResource, error) {
configurationOwners := []models.K8sConfigurationOwnerResource{}
for _, pod := range pods {
if isPodUsingConfigMap(&pod, configMap) {
kind := "Pod"
name := pod.Name
if pod.Namespace == configMap.Namespace {
if isPodUsingConfigMap(&pod, configMap.Name) {
application, err := kcl.ConvertPodToApplication(pod, PortainerApplicationResources{
ReplicaSets: replicaSets,
}, false)
if err != nil {
return nil, err
}
if len(pod.OwnerReferences) > 0 {
kind = pod.OwnerReferences[0].Kind
name = pod.OwnerReferences[0].Name
if application != nil {
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
Name: application.Name,
ResourceKind: application.Kind,
Id: application.UID,
})
}
}
if isReplicaSetOwner(pod) {
updateOwnerReferenceToDeployment(&pod, replicaSets)
}
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
Name: name,
ResourceKind: kind,
})
}
}
@@ -461,23 +501,23 @@ func (kcl *KubeClient) GetApplicationConfigurationOwnersFromConfigMap(configMap
func (kcl *KubeClient) GetApplicationConfigurationOwnersFromSecret(secret models.K8sSecret, pods []corev1.Pod, replicaSets []appsv1.ReplicaSet) ([]models.K8sConfigurationOwnerResource, error) {
configurationOwners := []models.K8sConfigurationOwnerResource{}
for _, pod := range pods {
if isPodUsingSecret(&pod, secret) {
kind := "Pod"
name := pod.Name
if pod.Namespace == secret.Namespace {
if isPodUsingSecret(&pod, secret.Name) {
application, err := kcl.ConvertPodToApplication(pod, PortainerApplicationResources{
ReplicaSets: replicaSets,
}, false)
if err != nil {
return nil, err
}
if len(pod.OwnerReferences) > 0 {
kind = pod.OwnerReferences[0].Kind
name = pod.OwnerReferences[0].Name
if application != nil {
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
Name: application.Name,
ResourceKind: application.Kind,
Id: application.UID,
})
}
}
if isReplicaSetOwner(pod) {
updateOwnerReferenceToDeployment(&pod, replicaSets)
}
configurationOwners = append(configurationOwners, models.K8sConfigurationOwnerResource{
Name: name,
ResourceKind: kind,
})
}
}
+7
View File
@@ -47,6 +47,13 @@ type (
}
)
func NewKubeClientFromClientset(cli *kubernetes.Clientset) *KubeClient {
return &KubeClient{
cli: cli,
instanceID: "",
}
}
// NewClientFactory returns a new instance of a ClientFactory
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*ClientFactory, error) {
if userSessionTimeout == "" {
+26 -22
View File
@@ -7,7 +7,6 @@ import (
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -96,28 +95,35 @@ func parseConfigMap(configMap *corev1.ConfigMap, withData bool) models.K8sConfig
return result
}
// SetConfigMapsIsUsed combines the config maps with the applications that use them.
// CombineConfigMapsWithApplications combines the config maps with the applications that use them.
// the function fetches all the pods and replica sets in the cluster and checks if the config map is used by any of the pods.
// if the config map is used by a pod, the application that uses the pod is added to the config map.
// otherwise, the config map is returned as is.
func (kcl *KubeClient) SetConfigMapsIsUsed(configMaps *[]models.K8sConfigMap) error {
func (kcl *KubeClient) CombineConfigMapsWithApplications(configMaps []models.K8sConfigMap) ([]models.K8sConfigMap, error) {
updatedConfigMaps := make([]models.K8sConfigMap, len(configMaps))
portainerApplicationResources, err := kcl.fetchAllApplicationsListResources("", metav1.ListOptions{})
if err != nil {
return fmt.Errorf("an error occurred during the SetConfigMapsIsUsed operation, unable to fetch Portainer application resources. Error: %w", err)
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to fetch pods and replica sets. Error: %w", err)
}
for i := range *configMaps {
configMap := &(*configMaps)[i]
for index, configMap := range configMaps {
updatedConfigMap := configMap
for _, pod := range portainerApplicationResources.Pods {
if isPodUsingConfigMap(&pod, *configMap) {
configMap.IsUsed = true
break
}
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromConfigMap(configMap, portainerApplicationResources.Pods, portainerApplicationResources.ReplicaSets)
if err != nil {
return nil, fmt.Errorf("an error occurred during the CombineConfigMapsWithApplications operation, unable to get applications from config map. Error: %w", err)
}
if len(applicationConfigurationOwners) > 0 {
updatedConfigMap.ConfigurationOwnerResources = applicationConfigurationOwners
updatedConfigMap.IsUsed = true
}
updatedConfigMaps[index] = updatedConfigMap
}
return nil
return updatedConfigMaps, nil
}
// CombineConfigMapWithApplications combines the config map with the applications that use it.
@@ -135,22 +141,20 @@ func (kcl *KubeClient) CombineConfigMapWithApplications(configMap models.K8sConf
break
}
var replicaSets *appsv1.ReplicaSetList
if containsReplicaSetOwner {
replicaSets, err = kcl.cli.AppsV1().ReplicaSets(configMap.Namespace).List(context.Background(), metav1.ListOptions{})
replicaSets, err := kcl.cli.AppsV1().ReplicaSets(configMap.Namespace).List(context.Background(), metav1.ListOptions{})
if err != nil {
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get replica sets. Error: %w", err)
}
}
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromConfigMap(configMap, pods.Items, replicaSets.Items)
if err != nil {
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get applications from config map. Error: %w", err)
}
applicationConfigurationOwners, err := kcl.GetApplicationConfigurationOwnersFromConfigMap(configMap, pods.Items, replicaSets.Items)
if err != nil {
return models.K8sConfigMap{}, fmt.Errorf("an error occurred during the CombineConfigMapWithApplications operation, unable to get applications from config map. Error: %w", err)
}
if len(applicationConfigurationOwners) > 0 {
configMap.ConfigurationOwnerResources = applicationConfigurationOwners
configMap.IsUsed = true
if len(applicationConfigurationOwners) > 0 {
configMap.ConfigurationOwnerResources = applicationConfigurationOwners
}
}
return configMap, nil
+6 -15
View File
@@ -7,7 +7,6 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
models "github.com/portainer/portainer/api/http/models/kubernetes"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
@@ -236,20 +235,16 @@ func (kcl *KubeClient) fetchResourcesWithOwnerReferences(namespace string, podLi
}
// isPodUsingConfigMap checks if a pod is using a specific ConfigMap
func isPodUsingConfigMap(pod *corev1.Pod, configMap models.K8sConfigMap) bool {
if pod.Namespace != configMap.Namespace {
return false
}
func isPodUsingConfigMap(pod *corev1.Pod, configMapName string) bool {
for _, volume := range pod.Spec.Volumes {
if volume.ConfigMap != nil && volume.ConfigMap.Name == configMap.Name {
if volume.ConfigMap != nil && volume.ConfigMap.Name == configMapName {
return true
}
}
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil && env.ValueFrom.ConfigMapKeyRef.Name == configMap.Name {
if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil && env.ValueFrom.ConfigMapKeyRef.Name == configMapName {
return true
}
}
@@ -259,20 +254,16 @@ func isPodUsingConfigMap(pod *corev1.Pod, configMap models.K8sConfigMap) bool {
}
// isPodUsingSecret checks if a pod is using a specific Secret
func isPodUsingSecret(pod *corev1.Pod, secret models.K8sSecret) bool {
if pod.Namespace != secret.Namespace {
return false
}
func isPodUsingSecret(pod *corev1.Pod, secretName string) bool {
for _, volume := range pod.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == secret.Name {
if volume.Secret != nil && volume.Secret.SecretName == secretName {
return true
}
}
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name == secret.Name {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.Name == secretName {
return true
}
}

Some files were not shown because too many files have changed in this diff Show More