1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

Compare commits

..

1 commit

Author SHA1 Message Date
Ali
f60e13adbb
chore: bump version to 2.30.0 (#735) 2025-05-15 09:06:22 +12:00
395 changed files with 3808 additions and 15250 deletions

View file

@ -94,20 +94,11 @@ body:
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.
multiple: false
options:
- '2.31.3'
- '2.31.2'
- '2.31.1'
- '2.31.0'
- '2.30.1'
- '2.30.0'
- '2.29.2'
- '2.29.1'
- '2.29.0'
- '2.28.1'
- '2.28.0'
- '2.27.9'
- '2.27.8'
- '2.27.7'
- '2.27.6'
- '2.27.5'
- '2.27.4'

View file

@ -61,8 +61,6 @@ func CLIFlags() *portainer.CLIFlags {
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
}
}

View file

@ -52,7 +52,6 @@ import (
"github.com/portainer/portainer/pkg/libhelm"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/portainer/portainer/pkg/validate"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
@ -331,18 +330,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
}
trustedOrigins := []string{}
if *flags.TrustedOrigins != "" {
// validate if the trusted origins are valid urls
for _, origin := range strings.Split(*flags.TrustedOrigins, ",") {
if !validate.IsTrustedOrigin(origin) {
log.Fatal().Str("trusted_origin", origin).Msg("invalid url for trusted origin. Please check the trusted origins flag.")
}
trustedOrigins = append(trustedOrigins, origin)
}
}
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
@ -383,8 +370,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
gitService := git.NewService(shutdownCtx)
// Setting insecureSkipVerify to true to preserve the old behaviour.
openAMTService := openamt.NewService(true)
openAMTService := openamt.NewService()
cryptoService := &crypto.Service{}
@ -559,7 +545,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
Status: applicationStatus,
BindAddress: *flags.Addr,
BindAddressHTTPS: *flags.AddrHTTPS,
CSP: *flags.CSP,
HTTPEnabled: sslDBSettings.HTTPEnabled,
AssetsPath: *flags.Assets,
DataStore: dataStore,
@ -593,7 +578,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
PendingActionsService: pendingActionsService,
PlatformService: platformService,
PullLimitCheckDisabled: *flags.PullLimitCheckDisabled,
TrustedOrigins: trustedOrigins,
}
}

View file

@ -138,8 +138,6 @@ func (connection *DbConnection) Open() error {
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
FreelistType: bolt.FreelistMapType,
NoFreelistSync: true,
})
if err != nil {
return err

View file

@ -10,7 +10,7 @@ type BaseCRUD[T any, I constraints.Integer] interface {
Create(element *T) error
Read(ID I) (*T, error)
Exists(ID I) (bool, error)
ReadAll(predicates ...func(T) bool) ([]T, error)
ReadAll() ([]T, error)
Update(ID I, element *T) error
Delete(ID I) error
}
@ -56,13 +56,12 @@ func (service BaseDataService[T, I]) Exists(ID I) (bool, error) {
return exists, err
}
// ReadAll retrieves all the elements that satisfy all the provided predicates.
func (service BaseDataService[T, I]) ReadAll(predicates ...func(T) bool) ([]T, error) {
func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
return collection, service.Connection.ViewTx(func(tx portainer.Transaction) error {
var err error
collection, err = service.Tx(tx).ReadAll(predicates...)
collection, err = service.Tx(tx).ReadAll()
return err
})

View file

@ -1,92 +0,0 @@
package dataservices
import (
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/slicesx"
"github.com/stretchr/testify/require"
)
type testObject struct {
ID int
Value int
}
type mockConnection struct {
store map[int]testObject
portainer.Connection
}
func (m mockConnection) UpdateObject(bucket string, key []byte, value interface{}) error {
obj := value.(*testObject)
m.store[obj.ID] = *obj
return nil
}
func (m mockConnection) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
for _, v := range m.store {
if _, err := appendFn(&v); err != nil {
return err
}
}
return nil
}
func (m mockConnection) UpdateTx(fn func(portainer.Transaction) error) error {
return fn(m)
}
func (m mockConnection) ViewTx(fn func(portainer.Transaction) error) error {
return fn(m)
}
func (m mockConnection) ConvertToKey(v int) []byte {
return []byte(strconv.Itoa(v))
}
func TestReadAll(t *testing.T) {
service := BaseDataService[testObject, int]{
Bucket: "testBucket",
Connection: mockConnection{store: make(map[int]testObject)},
}
data := []testObject{
{ID: 1, Value: 1},
{ID: 2, Value: 2},
{ID: 3, Value: 3},
{ID: 4, Value: 4},
{ID: 5, Value: 5},
}
for _, item := range data {
err := service.Update(item.ID, &item)
require.NoError(t, err)
}
// ReadAll without predicates
result, err := service.ReadAll()
require.NoError(t, err)
expected := append([]testObject{}, data...)
require.ElementsMatch(t, expected, result)
// ReadAll with predicates
hasLowID := func(obj testObject) bool { return obj.ID < 3 }
isEven := func(obj testObject) bool { return obj.Value%2 == 0 }
result, err = service.ReadAll(hasLowID, isEven)
require.NoError(t, err)
expected = slicesx.Filter(expected, hasLowID)
expected = slicesx.Filter(expected, isEven)
require.ElementsMatch(t, expected, result)
}

View file

@ -34,32 +34,13 @@ func (service BaseDataServiceTx[T, I]) Exists(ID I) (bool, error) {
return service.Tx.KeyExists(service.Bucket, identifier)
}
// ReadAll retrieves all the elements that satisfy all the provided predicates.
func (service BaseDataServiceTx[T, I]) ReadAll(predicates ...func(T) bool) ([]T, error) {
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
if len(predicates) == 0 {
return collection, service.Tx.GetAll(
service.Bucket,
new(T),
AppendFn(&collection),
)
}
filterFn := func(element T) bool {
for _, p := range predicates {
if !p(element) {
return false
}
}
return true
}
return collection, service.Tx.GetAll(
service.Bucket,
new(T),
FilterFn(&collection, filterFn),
AppendFn(&collection),
)
}

View file

@ -17,29 +17,11 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
}
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
es := group.Endpoints
group.Endpoints = nil // Clear deprecated field
err := service.Tx.CreateObject(
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},
)
group.Endpoints = es // Restore endpoints after create
return err
}
func (service ServiceTx) Update(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
es := group.Endpoints
group.Endpoints = nil // Clear deprecated field
err := service.BaseDataServiceTx.Update(ID, group)
group.Endpoints = es // Restore endpoints after update
return err
}

View file

@ -1,89 +0,0 @@
package edgestackstatus
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var _ dataservices.EdgeStackStatusService = &Service{}
const BucketName = "edge_stack_status"
type Service struct {
conn portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
func NewService(connection portainer.Connection) (*Service, error) {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
return &Service{conn: connection}, nil
}
func (s *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: s,
tx: tx,
}
}
func (s *Service) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Create(edgeStackID, endpointID, status)
})
}
func (s *Service) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
var element *portainer.EdgeStackStatusForEnv
return element, s.conn.ViewTx(func(tx portainer.Transaction) error {
var err error
element, err = s.Tx(tx).Read(edgeStackID, endpointID)
return err
})
}
func (s *Service) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
var collection = make([]portainer.EdgeStackStatusForEnv, 0)
return collection, s.conn.ViewTx(func(tx portainer.Transaction) error {
var err error
collection, err = s.Tx(tx).ReadAll(edgeStackID)
return err
})
}
func (s *Service) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Update(edgeStackID, endpointID, status)
})
}
func (s *Service) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Delete(edgeStackID, endpointID)
})
}
func (s *Service) DeleteAll(edgeStackID portainer.EdgeStackID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).DeleteAll(edgeStackID)
})
}
func (s *Service) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Clear(edgeStackID, relatedEnvironmentsIDs)
})
}
func (s *Service) key(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) []byte {
return append(s.conn.ConvertToKey(int(edgeStackID)), s.conn.ConvertToKey(int(endpointID))...)
}

View file

@ -1,95 +0,0 @@
package edgestackstatus
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var _ dataservices.EdgeStackStatusService = &Service{}
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
identifier := service.service.key(edgeStackID, endpointID)
return service.tx.CreateObjectWithStringId(BucketName, identifier, status)
}
func (s ServiceTx) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
var status portainer.EdgeStackStatusForEnv
identifier := s.service.key(edgeStackID, endpointID)
if err := s.tx.GetObject(BucketName, identifier, &status); err != nil {
return nil, err
}
return &status, nil
}
func (s ServiceTx) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
return nil, fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
}
return statuses, nil
}
func (s ServiceTx) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
identifier := s.service.key(edgeStackID, endpointID)
return s.tx.UpdateObject(BucketName, identifier, status)
}
func (s ServiceTx) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
identifier := s.service.key(edgeStackID, endpointID)
return s.tx.DeleteObject(BucketName, identifier)
}
func (s ServiceTx) DeleteAll(edgeStackID portainer.EdgeStackID) error {
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
return fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
}
for _, status := range statuses {
if err := s.tx.DeleteObject(BucketName, s.service.key(edgeStackID, status.EndpointID)); err != nil {
return fmt.Errorf("unable to delete EdgeStackStatus for EdgeStack %d and Endpoint %d: %w", edgeStackID, status.EndpointID, err)
}
}
return nil
}
func (s ServiceTx) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
for _, envID := range relatedEnvironmentsIDs {
existingStatus, err := s.Read(edgeStackID, envID)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return fmt.Errorf("unable to retrieve status for environment %d: %w", envID, err)
}
var deploymentInfo portainer.StackDeploymentInfo
if existingStatus != nil {
deploymentInfo = existingStatus.DeploymentInfo
}
if err := s.Update(edgeStackID, envID, &portainer.EdgeStackStatusForEnv{
EndpointID: envID,
Status: []portainer.EdgeStackDeploymentStatus{},
DeploymentInfo: deploymentInfo,
}); err != nil {
return err
}
}
return nil
}

View file

@ -112,13 +112,13 @@ func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID,
}
func (service *Service) AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.connection.ViewTx(func(tx portainer.Transaction) error {
return service.Tx(tx).AddEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
})
}
func (service *Service) RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.connection.ViewTx(func(tx portainer.Transaction) error {
return service.Tx(tx).RemoveEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
})
}

View file

@ -12,7 +12,6 @@ type (
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
EdgeStack() EdgeStackService
EdgeStackStatus() EdgeStackStatusService
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
@ -40,8 +39,8 @@ type (
Open() (newStore bool, err error)
Init() error
Close() error
UpdateTx(func(tx DataStoreTx) error) error
ViewTx(func(tx DataStoreTx) error) error
UpdateTx(func(DataStoreTx) error) error
ViewTx(func(DataStoreTx) error) error
MigrateData() error
Rollback(force bool) error
CheckCurrentEdition() error
@ -90,16 +89,6 @@ type (
BucketName() string
}
EdgeStackStatusService interface {
Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error)
ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error)
Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error
DeleteAll(edgeStackID portainer.EdgeStackID) error
Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error
}
// EndpointService represents a service for managing environment(endpoint) data
EndpointService interface {
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)

View file

@ -51,20 +51,3 @@ func (service *Service) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portai
return snapshot, err
}
func (service *Service) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
var snapshot *portainer.SnapshotRawMessage
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
var err error
snapshot, err = service.Tx(tx).ReadRawMessage(ID)
return err
})
return snapshot, err
}
func (service *Service) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View file

@ -35,19 +35,3 @@ func (service ServiceTx) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*porta
return &snapshot.Snapshot, nil
}
func (service ServiceTx) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
var snapshot = portainer.SnapshotRawMessage{}
identifier := service.Connection.ConvertToKey(int(ID))
if err := service.Tx.GetObject(service.Bucket, identifier, &snapshot); err != nil {
return nil, err
}
return &snapshot, nil
}
func (service ServiceTx) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View file

@ -40,11 +40,13 @@ func (store *Store) MigrateData() error {
}
// before we alter anything in the DB, create a backup
if _, err := store.Backup(""); err != nil {
_, err = store.Backup("")
if err != nil {
return errors.Wrap(err, "while backing up database")
}
if err := store.FailSafeMigrate(migrator, version); err != nil {
err = store.FailSafeMigrate(migrator, version)
if err != nil {
err = errors.Wrap(err, "failed to migrate database")
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
@ -83,9 +85,7 @@ func (store *Store) newMigratorParameters(version *models.Version, flags *portai
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
EdgeStackService: store.EdgeStackService,
EdgeStackStatusService: store.EdgeStackStatusService,
EdgeJobService: store.EdgeJobService,
EdgeGroupService: store.EdgeGroupService,
TunnelServerService: store.TunnelServerService,
PendingActionsService: store.PendingActionsService,
}
@ -140,7 +140,8 @@ func (store *Store) connectionRollback(force bool) error {
}
}
if err := store.Restore(); err != nil {
err := store.Restore()
if err != nil {
return err
}

View file

@ -1,31 +0,0 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateEdgeStacksStatuses_2_31_0() error {
edgeStacks, err := m.edgeStackService.EdgeStacks()
if err != nil {
return err
}
for _, edgeStack := range edgeStacks {
for envID, status := range edgeStack.Status {
if err := m.edgeStackStatusService.Create(edgeStack.ID, envID, &portainer.EdgeStackStatusForEnv{
EndpointID: envID,
Status: status.Status,
DeploymentInfo: status.DeploymentInfo,
ReadyRePullImage: status.ReadyRePullImage,
}); err != nil {
return err
}
}
edgeStack.Status = nil
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
return err
}
}
return nil
}

View file

@ -1,33 +0,0 @@
package migrator
import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
perrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/internal/endpointutils"
)
func (m *Migrator) addEndpointRelationForEdgeAgents_2_32_0() error {
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpointutils.IsEdgeEndpoint(&endpoint) {
_, err := m.endpointRelationService.EndpointRelation(endpoint.ID)
if err != nil && errors.Is(err, perrors.ErrObjectNotFound) {
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: make(map[portainer.EdgeStackID]bool),
}
if err := m.endpointRelationService.Create(relation); err != nil {
return err
}
}
}
}
return nil
}

View file

@ -1,23 +0,0 @@
package migrator
import (
"github.com/portainer/portainer/api/roar"
)
func (m *Migrator) migrateEdgeGroupEndpointsToRoars_2_33_0() error {
egs, err := m.edgeGroupService.ReadAll()
if err != nil {
return err
}
for _, eg := range egs {
eg.EndpointIDs = roar.FromSlice(eg.Endpoints)
eg.Endpoints = nil
if err := m.edgeGroupService.Update(eg.ID, &eg); err != nil {
return err
}
}
return nil
}

View file

@ -3,13 +3,12 @@ package migrator
import (
"errors"
"github.com/Masterminds/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
@ -28,8 +27,6 @@ import (
"github.com/portainer/portainer/api/dataservices/user"
"github.com/portainer/portainer/api/dataservices/version"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
)
@ -59,9 +56,7 @@ type (
authorizationService *authorization.Service
dockerhubService *dockerhub.Service
edgeStackService *edgestack.Service
edgeStackStatusService *edgestackstatus.Service
edgeJobService *edgejob.Service
edgeGroupService *edgegroup.Service
TunnelServerService *tunnelserver.Service
pendingActionsService *pendingactions.Service
}
@ -89,9 +84,7 @@ type (
AuthorizationService *authorization.Service
DockerhubService *dockerhub.Service
EdgeStackService *edgestack.Service
EdgeStackStatusService *edgestackstatus.Service
EdgeJobService *edgejob.Service
EdgeGroupService *edgegroup.Service
TunnelServerService *tunnelserver.Service
PendingActionsService *pendingactions.Service
}
@ -121,15 +114,12 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService,
edgeStackService: parameters.EdgeStackService,
edgeStackStatusService: parameters.EdgeStackStatusService,
edgeJobService: parameters.EdgeJobService,
edgeGroupService: parameters.EdgeGroupService,
TunnelServerService: parameters.TunnelServerService,
pendingActionsService: parameters.PendingActionsService,
}
migrator.initMigrations()
return migrator
}
@ -252,12 +242,6 @@ func (m *Migrator) initMigrations() {
m.migratePendingActionsDataForDB130,
)
m.addMigrations("2.31.0", m.migrateEdgeStacksStatuses_2_31_0)
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
m.addMigrations("2.33.0", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
// Add new migrations above...
// One function per migration, each versions migration funcs in the same file.
}

View file

@ -13,7 +13,6 @@ import (
"github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
@ -40,8 +39,6 @@ import (
"github.com/segmentio/encoding/json"
)
var _ dataservices.DataStore = &Store{}
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
@ -54,7 +51,6 @@ type Store struct {
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service
EdgeStackStatusService *edgestackstatus.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
@ -113,12 +109,6 @@ func (store *Store) initServices() error {
store.EdgeStackService = edgeStackService
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx)
edgeStackStatusService, err := edgestackstatus.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackStatusService = edgeStackStatusService
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
return err
@ -279,10 +269,6 @@ func (store *Store) EdgeStack() dataservices.EdgeStackService {
return store.EdgeStackService
}
func (store *Store) EdgeStackStatus() dataservices.EdgeStackStatusService {
return store.EdgeStackStatusService
}
// Environment(Endpoint) gives access to the Environment(Endpoint) data management layer
func (store *Store) Endpoint() dataservices.EndpointService {
return store.EndpointService

View file

@ -32,10 +32,6 @@ func (tx *StoreTx) EdgeStack() dataservices.EdgeStackService {
return tx.store.EdgeStackService.Tx(tx.tx)
}
func (tx *StoreTx) EdgeStackStatus() dataservices.EdgeStackStatusService {
return tx.store.EdgeStackStatusService.Tx(tx.tx)
}
func (tx *StoreTx) Endpoint() dataservices.EndpointService {
return tx.store.EndpointService.Tx(tx.tx)
}

View file

@ -8,7 +8,6 @@
}
],
"edge_stack": null,
"edge_stack_status": null,
"edgegroups": null,
"edgejobs": null,
"endpoint_groups": [
@ -121,10 +120,6 @@
"Ecr": {
"Region": ""
},
"Github": {
"OrganisationName": "",
"UseOrganisation": false
},
"Gitlab": {
"InstanceURL": "",
"ProjectId": 0,
@ -615,7 +610,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.32.0",
"KubectlShellImage": "portainer/kubectl-shell:2.30.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@ -683,11 +678,14 @@
"Images": null,
"Info": {
"Architecture": "",
"BridgeNfIp6tables": false,
"BridgeNfIptables": false,
"CDISpecDirs": null,
"CPUSet": false,
"CPUShares": false,
"CgroupDriver": "",
"ContainerdCommit": {
"Expected": "",
"ID": ""
},
"Containers": 0,
@ -711,6 +709,7 @@
"IndexServerAddress": "",
"InitBinary": "",
"InitCommit": {
"Expected": "",
"ID": ""
},
"Isolation": "",
@ -739,6 +738,7 @@
},
"RegistryConfig": null,
"RuncCommit": {
"Expected": "",
"ID": ""
},
"Runtimes": null,
@ -780,7 +780,6 @@
"ImageCount": 9,
"IsPodman": false,
"NodeCount": 0,
"PerformanceMetrics": null,
"RunningContainerCount": 5,
"ServiceCount": 0,
"StackCount": 2,
@ -944,7 +943,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.32.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.30.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}

View file

@ -58,15 +58,7 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
dst := t.TempDir()
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err := service.CloneRepository(
dst,
repositoryUrl,
tt.args.referenceName,
"",
"",
gittypes.GitCredentialAuthType_Basic,
false,
)
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
})
@ -81,15 +73,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
dst := t.TempDir()
err := service.CloneRepository(
dst,
privateAzureRepoURL,
"refs/heads/main",
"",
pat,
gittypes.GitCredentialAuthType_Basic,
false,
)
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
@ -100,14 +84,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService(context.TODO())
id, err := service.LatestCommitID(
privateAzureRepoURL,
"refs/heads/main",
"",
pat,
gittypes.GitCredentialAuthType_Basic,
false,
)
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
}
@ -119,14 +96,7 @@ func TestService_ListRefs_Azure(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := NewService(context.TODO())
refs, err := service.ListRefs(
privateAzureRepoURL,
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
)
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
}
@ -138,8 +108,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
time.Sleep(2 * time.Second)
}
@ -277,17 +247,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(
tt.args.repositoryUrl,
tt.args.referenceName,
tt.args.username,
tt.args.password,
gittypes.GitCredentialAuthType_Basic,
false,
false,
tt.extensions,
false,
)
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, false, tt.extensions, false)
if tt.expect.shouldFail {
assert.Error(t, err)
if tt.expect.err != nil {
@ -310,28 +270,8 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListFiles(
privateAzureRepoURL,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListFiles(
privateAzureRepoURL,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false)
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, false, []string{}, false)
time.Sleep(2 * time.Second)
}

View file

@ -19,7 +19,6 @@ type CloneOptions struct {
ReferenceName string
Username string
Password string
AuthType gittypes.GitCredentialAuthType
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
}
@ -43,15 +42,7 @@ func CloneWithBackup(gitService portainer.GitService, fileService portainer.File
cleanUp = true
if err := gitService.CloneRepository(
options.ProjectPath,
options.URL,
options.ReferenceName,
options.Username,
options.Password,
options.AuthType,
options.TLSSkipVerify,
); err != nil {
if err := gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify); err != nil {
cleanUp = false
if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil {
log.Warn().Err(err).Msg("failed restoring backup folder")

View file

@ -7,14 +7,12 @@ import (
"strings"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/pkg/errors"
@ -35,7 +33,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
URL: opt.repositoryUrl,
Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.authType, opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
Tags: git.NoTags,
}
@ -53,10 +51,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
}
if !c.preserveGitDirectory {
err := os.RemoveAll(filepath.Join(dst, ".git"))
if err != nil {
log.Error().Err(err).Msg("failed to remove .git directory")
}
os.RemoveAll(filepath.Join(dst, ".git"))
}
return nil
@ -69,7 +64,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
})
listOptions := &git.ListOptions{
Auth: getAuth(opt.authType, opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
}
@ -99,23 +94,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
return "", errors.Errorf("could not find ref %q in the repository", opt.referenceName)
}
func getAuth(authType gittypes.GitCredentialAuthType, username, password string) transport.AuthMethod {
if password == "" {
return nil
}
switch authType {
case gittypes.GitCredentialAuthType_Basic:
return getBasicAuth(username, password)
case gittypes.GitCredentialAuthType_Token:
return getTokenAuth(password)
default:
log.Warn().Msg("unknown git credentials authorization type, defaulting to None")
return nil
}
}
func getBasicAuth(username, password string) *githttp.BasicAuth {
func getAuth(username, password string) *githttp.BasicAuth {
if password != "" {
if username == "" {
username = "token"
@ -129,15 +108,6 @@ func getBasicAuth(username, password string) *githttp.BasicAuth {
return nil
}
func getTokenAuth(token string) *githttp.TokenAuth {
if token != "" {
return &githttp.TokenAuth{
Token: token,
}
}
return nil
}
func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, error) {
rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
Name: "origin",
@ -145,7 +115,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
})
listOptions := &git.ListOptions{
Auth: getAuth(opt.authType, opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
}
@ -173,7 +143,7 @@ func (c *gitClient) listFiles(ctx context.Context, opt fetchOption) ([]string, e
Depth: 1,
SingleBranch: true,
ReferenceName: plumbing.ReferenceName(opt.referenceName),
Auth: getAuth(opt.authType, opt.username, opt.password),
Auth: getAuth(opt.username, opt.password),
InsecureSkipTLS: opt.tlsSkipVerify,
Tags: git.NoTags,
}

View file

@ -2,8 +2,6 @@ package git
import (
"context"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
@ -26,15 +24,7 @@ func TestService_ClonePrivateRepository_GitHub(t *testing.T) {
dst := t.TempDir()
repositoryUrl := privateGitRepoURL
err := service.CloneRepository(
dst,
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
)
err := service.CloneRepository(dst, repositoryUrl, "refs/heads/main", username, accessToken, false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
@ -47,14 +37,7 @@ func TestService_LatestCommitID_GitHub(t *testing.T) {
service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL
id, err := service.LatestCommitID(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
)
id, err := service.LatestCommitID(repositoryUrl, "refs/heads/main", username, accessToken, false)
assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
}
@ -67,7 +50,7 @@ func TestService_ListRefs_GitHub(t *testing.T) {
service := newService(context.TODO(), 0, 0)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
}
@ -80,8 +63,8 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
repositoryUrl := privateGitRepoURL
go service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
go service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListRefs(repositoryUrl, username, accessToken, false, false)
time.Sleep(2 * time.Second)
}
@ -219,17 +202,7 @@ func TestService_ListFiles_GitHub(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(
tt.args.repositoryUrl,
tt.args.referenceName,
tt.args.username,
tt.args.password,
gittypes.GitCredentialAuthType_Basic,
false,
false,
tt.extensions,
false,
)
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, false, tt.extensions, false)
if tt.expect.shouldFail {
assert.Error(t, err)
if tt.expect.err != nil {
@ -253,28 +226,8 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
go service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
time.Sleep(2 * time.Second)
}
@ -287,18 +240,8 @@ func TestService_purgeCache_Github(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService(context.TODO())
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len())
@ -318,18 +261,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
// 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
service := newService(context.TODO(), 2, 40*timeout)
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListRefs(repositoryUrl, username, accessToken, false, false)
service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len())
@ -360,12 +293,12 @@ func TestService_HardRefresh_ListRefs_GitHub(t *testing.T) {
service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len())
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false)
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len())
}
@ -378,46 +311,26 @@ func TestService_HardRefresh_ListRefs_And_RemoveAllCaches_GitHub(t *testing.T) {
service := newService(context.TODO(), 2, 0)
repositoryUrl := privateGitRepoURL
refs, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
refs, err := service.ListRefs(repositoryUrl, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
assert.Equal(t, 1, service.repoRefCache.Len())
files, err := service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len())
files, err = service.ListFiles(
repositoryUrl,
"refs/heads/test",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
files, err = service.ListFiles(repositoryUrl, "refs/heads/test", username, accessToken, false, false, []string{}, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 2, service.repoFileCache.Len())
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, false, false)
_, err = service.ListRefs(repositoryUrl, username, "fake-token", false, false)
assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len())
_, err = service.ListRefs(repositoryUrl, username, "fake-token", gittypes.GitCredentialAuthType_Basic, true, false)
_, err = service.ListRefs(repositoryUrl, username, "fake-token", true, false)
assert.Error(t, err)
assert.Equal(t, 1, service.repoRefCache.Len())
// The relevant file caches should be removed too
@ -431,72 +344,12 @@ func TestService_HardRefresh_ListFiles_GitHub(t *testing.T) {
accessToken := getRequiredValue(t, "GITHUB_PAT")
username := getRequiredValue(t, "GITHUB_USERNAME")
repositoryUrl := privateGitRepoURL
files, err := service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
files, err := service.ListFiles(repositoryUrl, "refs/heads/main", username, accessToken, false, false, []string{}, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(files), 1)
assert.Equal(t, 1, service.repoFileCache.Len())
_, err = service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
"fake-token",
gittypes.GitCredentialAuthType_Basic,
false,
true,
[]string{},
false,
)
_, err = service.ListFiles(repositoryUrl, "refs/heads/main", username, "fake-token", false, true, []string{}, false)
assert.Error(t, err)
assert.Equal(t, 0, service.repoFileCache.Len())
}
func TestService_CloneRepository_TokenAuth(t *testing.T) {
ensureIntegrationTest(t)
service := newService(context.TODO(), 2, 0)
var requests []*http.Request
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests = append(requests, r)
}))
accessToken := "test_access_token"
username := "test_username"
repositoryUrl := testServer.URL
// Since we aren't hitting a real git server we ignore the error
_ = service.CloneRepository(
"test_dir",
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Token,
false,
)
testServer.Close()
if len(requests) != 1 {
t.Fatalf("expected 1 request sent but got %d", len(requests))
}
gotAuthHeader := requests[0].Header.Get("Authorization")
if gotAuthHeader == "" {
t.Fatal("no Authorization header in git request")
}
expectedAuthHeader := "Bearer test_access_token"
if gotAuthHeader != expectedAuthHeader {
t.Fatalf("expected Authorization header %q but got %q", expectedAuthHeader, gotAuthHeader)
}
}

View file

@ -38,7 +38,7 @@ func Test_ClonePublicRepository_Shallow(t *testing.T) {
dir := t.TempDir()
t.Logf("Cloning into %s", dir)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
assert.NoError(t, err)
assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}
@ -50,7 +50,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
dir := t.TempDir()
t.Logf("Cloning into %s", dir)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", false)
assert.NoError(t, err)
assert.NoDirExists(t, filepath.Join(dir, ".git"))
}
@ -84,7 +84,7 @@ func Test_latestCommitID(t *testing.T) {
repositoryURL := setup(t)
referenceName := "refs/heads/main"
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
id, err := service.LatestCommitID(repositoryURL, referenceName, "", "", false)
assert.NoError(t, err)
assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
@ -95,7 +95,7 @@ func Test_ListRefs(t *testing.T) {
repositoryURL := setup(t)
fs, err := service.ListRefs(repositoryURL, "", "", gittypes.GitCredentialAuthType_Basic, false, false)
fs, err := service.ListRefs(repositoryURL, "", "", false, false)
assert.NoError(t, err)
assert.Equal(t, []string{"refs/heads/main"}, fs)
@ -107,17 +107,7 @@ func Test_ListFiles(t *testing.T) {
repositoryURL := setup(t)
referenceName := "refs/heads/main"
fs, err := service.ListFiles(
repositoryURL,
referenceName,
"",
"",
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{".yml"},
false,
)
fs, err := service.ListFiles(repositoryURL, referenceName, "", "", false, false, []string{".yml"}, false)
assert.NoError(t, err)
assert.Equal(t, []string{"docker-compose.yml"}, fs)
@ -265,7 +255,7 @@ func Test_listFilesPrivateRepository(t *testing.T) {
name: "list tree with real repository and head ref but no credential",
args: fetchOption{
baseOption: baseOption{
repositoryUrl: privateGitRepoURL,
repositoryUrl: privateGitRepoURL + "fake",
username: "",
password: "",
},

View file

@ -8,7 +8,6 @@ import (
"time"
lru "github.com/hashicorp/golang-lru"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log"
"golang.org/x/sync/singleflight"
)
@ -23,7 +22,6 @@ type baseOption struct {
repositoryUrl string
username string
password string
authType gittypes.GitCredentialAuthType
tlsSkipVerify bool
}
@ -125,22 +123,13 @@ func (service *Service) timerHasStopped() bool {
// CloneRepository clones a git repository using the specified URL in the specified
// destination folder.
func (service *Service) CloneRepository(
destination,
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) error {
func (service *Service) CloneRepository(destination, repositoryURL, referenceName, username, password string, tlsSkipVerify bool) error {
options := cloneOption{
fetchOption: fetchOption{
baseOption: baseOption{
repositoryUrl: repositoryURL,
username: username,
password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify,
},
referenceName: referenceName,
@ -166,20 +155,12 @@ func (service *Service) cloneRepository(destination string, options cloneOption)
}
// LatestCommitID returns SHA1 of the latest commit of the specified reference
func (service *Service) LatestCommitID(
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) (string, error) {
func (service *Service) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
options := fetchOption{
baseOption: baseOption{
repositoryUrl: repositoryURL,
username: username,
password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify,
},
referenceName: referenceName,
@ -189,14 +170,7 @@ func (service *Service) LatestCommitID(
}
// ListRefs will list target repository's references without cloning the repository
func (service *Service) ListRefs(
repositoryURL,
username,
password string,
authType gittypes.GitCredentialAuthType,
hardRefresh bool,
tlsSkipVerify bool,
) ([]string, error) {
func (service *Service) ListRefs(repositoryURL, username, password string, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
refCacheKey := generateCacheKey(repositoryURL, username, password, strconv.FormatBool(tlsSkipVerify))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
@ -222,7 +196,6 @@ func (service *Service) ListRefs(
repositoryUrl: repositoryURL,
username: username,
password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify,
}
@ -242,62 +215,18 @@ var singleflightGroup = &singleflight.Group{}
// ListFiles will list all the files of the target repository with specific extensions.
// If extension is not provided, it will list all the files under the target repository
func (service *Service) ListFiles(
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
dirOnly,
hardRefresh bool,
includedExts []string,
tlsSkipVerify bool,
) ([]string, error) {
repoKey := generateCacheKey(
repositoryURL,
referenceName,
username,
password,
strconv.FormatBool(tlsSkipVerify),
strconv.Itoa(int(authType)),
strconv.FormatBool(dirOnly),
)
func (service *Service) ListFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, includedExts []string, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
fs, err, _ := singleflightGroup.Do(repoKey, func() (any, error) {
return service.listFiles(
repositoryURL,
referenceName,
username,
password,
authType,
dirOnly,
hardRefresh,
tlsSkipVerify,
)
return service.listFiles(repositoryURL, referenceName, username, password, dirOnly, hardRefresh, tlsSkipVerify)
})
return filterFiles(fs.([]string), includedExts), err
}
func (service *Service) listFiles(
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
dirOnly,
hardRefresh bool,
tlsSkipVerify bool,
) ([]string, error) {
repoKey := generateCacheKey(
repositoryURL,
referenceName,
username,
password,
strconv.FormatBool(tlsSkipVerify),
strconv.Itoa(int(authType)),
strconv.FormatBool(dirOnly),
)
func (service *Service) listFiles(repositoryURL, referenceName, username, password string, dirOnly, hardRefresh bool, tlsSkipVerify bool) ([]string, error) {
repoKey := generateCacheKey(repositoryURL, referenceName, username, password, strconv.FormatBool(tlsSkipVerify), strconv.FormatBool(dirOnly))
if service.cacheEnabled && hardRefresh {
// Should remove the cache explicitly, so that the following normal list can show the correct result
@ -318,7 +247,6 @@ func (service *Service) listFiles(
repositoryUrl: repositoryURL,
username: username,
password: password,
authType: authType,
tlsSkipVerify: tlsSkipVerify,
},
referenceName: referenceName,

View file

@ -1,21 +1,12 @@
package gittypes
import (
"errors"
)
import "errors"
var (
ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct")
ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct")
)
type GitCredentialAuthType int
const (
GitCredentialAuthType_Basic GitCredentialAuthType = iota
GitCredentialAuthType_Token
)
// RepoConfig represents a configuration for a repo
type RepoConfig struct {
// The repo url
@ -33,11 +24,10 @@ type RepoConfig struct {
}
type GitAuthentication struct {
Username string
Password string
AuthorizationType GitCredentialAuthType
Username string
Password string
// Git credentials identifier when the value is not 0
// When the value is 0, Username, Password, and Authtype are set without using saved credential
// When the value is 0, Username and Password are set without using saved credential
// This is introduced since 2.15.0
GitCredentialID int `example:"0"`
}

View file

@ -29,14 +29,7 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
return false, "", errors.WithMessagef(err, "failed to get credentials for %v", objId)
}
newHash, err := gitService.LatestCommitID(
gitConfig.URL,
gitConfig.ReferenceName,
username,
password,
gittypes.GitCredentialAuthType_Basic,
gitConfig.TLSSkipVerify,
)
newHash, err := gitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, username, password, gitConfig.TLSSkipVerify)
if err != nil {
return false, "", errors.WithMessagef(err, "failed to fetch latest commit id of %v", objId)
}
@ -69,7 +62,6 @@ func UpdateGitObject(gitService portainer.GitService, objId string, gitConfig *g
cloneParams.auth = &gitAuth{
username: username,
password: password,
authType: gitConfig.Authentication.AuthorizationType,
}
}
@ -97,31 +89,14 @@ type cloneRepositoryParameters struct {
}
type gitAuth struct {
authType gittypes.GitCredentialAuthType
username string
password string
}
func cloneGitRepository(gitService portainer.GitService, cloneParams *cloneRepositoryParameters) error {
if cloneParams.auth != nil {
return gitService.CloneRepository(
cloneParams.toDir,
cloneParams.url,
cloneParams.ref,
cloneParams.auth.username,
cloneParams.auth.password,
cloneParams.auth.authType,
cloneParams.tlsSkipVerify,
)
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, cloneParams.auth.username, cloneParams.auth.password, cloneParams.tlsSkipVerify)
}
return gitService.CloneRepository(
cloneParams.toDir,
cloneParams.url,
cloneParams.ref,
"",
"",
gittypes.GitCredentialAuthType_Basic,
cloneParams.tlsSkipVerify,
)
return gitService.CloneRepository(cloneParams.toDir, cloneParams.url, cloneParams.ref, "", "", cloneParams.tlsSkipVerify)
}

View file

@ -32,9 +32,9 @@ type Service struct {
}
// NewService initializes a new service.
func NewService(insecureSkipVerify bool) *Service {
func NewService() *Service {
tlsConfig := crypto.CreateTLSConfiguration()
tlsConfig.InsecureSkipVerify = insecureSkipVerify
tlsConfig.InsecureSkipVerify = true
return &Service{
httpsClient: &http.Client{

View file

@ -2,7 +2,6 @@ package csrf
import (
"crypto/rand"
"errors"
"fmt"
"net/http"
"os"
@ -10,8 +9,7 @@ import (
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
gcsrf "github.com/gorilla/csrf"
"github.com/rs/zerolog/log"
gorillacsrf "github.com/gorilla/csrf"
"github.com/urfave/negroni"
)
@ -21,7 +19,7 @@ func SkipCSRFToken(w http.ResponseWriter) {
w.Header().Set(csrfSkipHeader, "1")
}
func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, error) {
func WithProtect(handler http.Handler) (http.Handler, error) {
// IsDockerDesktopExtension is used to check if we should skip csrf checks in the request bouncer (ShouldSkipCSRFCheck)
// DOCKER_EXTENSION is set to '1' in build/docker-extension/docker-compose.yml
isDockerDesktopExtension := false
@ -36,12 +34,10 @@ func WithProtect(handler http.Handler, trustedOrigins []string) (http.Handler, e
return nil, fmt.Errorf("failed to generate CSRF token: %w", err)
}
handler = gcsrf.Protect(
handler = gorillacsrf.Protect(
token,
gcsrf.Path("/"),
gcsrf.Secure(false),
gcsrf.TrustedOrigins(trustedOrigins),
gcsrf.ErrorHandler(withErrorHandler(trustedOrigins)),
gorillacsrf.Path("/"),
gorillacsrf.Secure(false),
)(handler)
return withSkipCSRF(handler, isDockerDesktopExtension), nil
@ -59,7 +55,7 @@ func withSendCSRFToken(handler http.Handler) http.Handler {
}
if statusCode := sw.Status(); statusCode >= 200 && statusCode < 300 {
sw.Header().Set("X-CSRF-Token", gcsrf.Token(r))
sw.Header().Set("X-CSRF-Token", gorillacsrf.Token(r))
}
})
@ -77,33 +73,9 @@ func withSkipCSRF(handler http.Handler, isDockerDesktopExtension bool) http.Hand
}
if skip {
r = gcsrf.UnsafeSkipCheck(r)
r = gorillacsrf.UnsafeSkipCheck(r)
}
handler.ServeHTTP(w, r)
})
}
func withErrorHandler(trustedOrigins []string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := gcsrf.FailureReason(r)
if errors.Is(err, gcsrf.ErrBadOrigin) || errors.Is(err, gcsrf.ErrBadReferer) || errors.Is(err, gcsrf.ErrNoReferer) {
log.Error().Err(err).
Str("request_url", r.URL.String()).
Str("host", r.Host).
Str("x_forwarded_proto", r.Header.Get("X-Forwarded-Proto")).
Str("forwarded", r.Header.Get("Forwarded")).
Str("origin", r.Header.Get("Origin")).
Str("referer", r.Header.Get("Referer")).
Strs("trusted_origins", trustedOrigins).
Msg("Failed to validate Origin or Referer")
}
http.Error(
w,
http.StatusText(http.StatusForbidden)+" - "+err.Error(),
http.StatusForbidden,
)
})
}

View file

@ -2,7 +2,6 @@ package auth
import (
"net/http"
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
@ -83,11 +82,6 @@ func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *h
}
}
// Clear any existing user caches
if user != nil {
handler.KubernetesClientFactory.ClearUserClientCache(strconv.Itoa(int(user.ID)))
}
if user != nil && isUserInitialAdmin(user) || settings.AuthenticationMethod == portainer.AuthenticationInternal {
return handler.authenticateInternal(rw, user, payload.Password)
}

View file

@ -8,7 +8,6 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux"
@ -24,18 +23,16 @@ type Handler struct {
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
KubernetesClientFactory *cli.ClientFactory
passwordStrengthChecker security.PasswordStrengthChecker
bouncer security.BouncerService
}
// NewHandler creates a handler to manage authentication operations.
func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker, kubernetesClientFactory *cli.ClientFactory) *Handler {
func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimiter, passwordStrengthChecker security.PasswordStrengthChecker) *Handler {
h := &Handler{
Router: mux.NewRouter(),
passwordStrengthChecker: passwordStrengthChecker,
bouncer: bouncer,
KubernetesClientFactory: kubernetesClientFactory,
}
h.Handle("/auth/oauth/validate",

View file

@ -2,7 +2,6 @@ package auth
import (
"net/http"
"strconv"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/logoutcontext"
@ -24,7 +23,6 @@ func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperro
if tokenData != nil {
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
handler.KubernetesClientFactory.ClearUserClientCache(strconv.Itoa(int(tokenData.ID)))
logoutcontext.Cancel(tokenData.Token)
}

View file

@ -33,28 +33,13 @@ type TestGitService struct {
targetFilePath string
}
func (g *TestGitService) CloneRepository(
destination string,
repositoryURL,
referenceName string,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) error {
func (g *TestGitService) CloneRepository(destination string, repositoryURL, referenceName string, username, password string, tlsSkipVerify bool) error {
time.Sleep(100 * time.Millisecond)
return createTestFile(g.targetFilePath)
}
func (g *TestGitService) LatestCommitID(
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) (string, error) {
func (g *TestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
return "", nil
}
@ -71,26 +56,11 @@ type InvalidTestGitService struct {
targetFilePath string
}
func (g *InvalidTestGitService) CloneRepository(
dest,
repoUrl,
refName,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) error {
func (g *InvalidTestGitService) CloneRepository(dest, repoUrl, refName, username, password string, tlsSkipVerify bool) error {
return errors.New("simulate network error")
}
func (g *InvalidTestGitService) LatestCommitID(
repositoryURL,
referenceName,
username,
password string,
authType gittypes.GitCredentialAuthType,
tlsSkipVerify bool,
) (string, error) {
func (g *InvalidTestGitService) LatestCommitID(repositoryURL, referenceName, username, password string, tlsSkipVerify bool) (string, error) {
return "", nil
}

View file

@ -71,7 +71,7 @@ func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Reques
customTemplates = filterByType(customTemplates, templateTypes)
if edge != nil {
customTemplates = slicesx.FilterInPlace(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
customTemplates = slicesx.Filter(customTemplates, func(customTemplate portainer.CustomTemplate) bool {
return customTemplate.EdgeTemplate == *edge
})
}

View file

@ -37,16 +37,14 @@ type customTemplateUpdatePayload struct {
RepositoryURL string `example:"https://github.com/openfaas/faas" validate:"required"`
// Reference name of a Git repository hosting the Stack file
RepositoryReferenceName string `example:"refs/heads/master"`
// Use authentication to clone the Git repository
// Use basic authentication to clone the Git repository
RepositoryAuthentication bool `example:"true"`
// Username used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0. Ignored if RepositoryAuthType is token
// and RepositoryGitCredentialID is 0
RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication or token used in token authentication.
// Required when RepositoryAuthentication is true and RepositoryGitCredentialID is 0
// Password used in basic authentication. Required when RepositoryAuthentication is true
// and RepositoryGitCredentialID is 0
RepositoryPassword string `example:"myGitPassword"`
// RepositoryAuthorizationType is the authorization type to use
RepositoryAuthorizationType gittypes.GitCredentialAuthType `example:"0"`
// GitCredentialID used to identify the bound git credential. Required when RepositoryAuthentication
// is true and RepositoryUsername/RepositoryPassword are not provided
RepositoryGitCredentialID int `example:"0"`
@ -184,15 +182,12 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
repositoryUsername := ""
repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if payload.RepositoryAuthentication {
repositoryUsername = payload.RepositoryUsername
repositoryPassword = payload.RepositoryPassword
repositoryAuthType = payload.RepositoryAuthorizationType
gitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
AuthorizationType: payload.RepositoryAuthorizationType,
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
@ -202,7 +197,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
ReferenceName: gitConfig.ReferenceName,
Username: repositoryUsername,
Password: repositoryPassword,
AuthType: repositoryAuthType,
TLSSkipVerify: gitConfig.TLSSkipVerify,
})
if err != nil {
@ -211,14 +205,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
defer cleanBackup()
commitHash, err := handler.GitService.LatestCommitID(
gitConfig.URL,
gitConfig.ReferenceName,
repositoryUsername,
repositoryPassword,
repositoryAuthType,
gitConfig.TLSSkipVerify,
)
commitHash, err := handler.GitService.LatestCommitID(gitConfig.URL, gitConfig.ReferenceName, repositoryUsername, repositoryPassword, gitConfig.TLSSkipVerify)
if err != nil {
return httperror.InternalServerError("Unable get latest commit id", fmt.Errorf("failed to fetch latest commit id of the template %v: %w", customTemplate.ID, err))
}

View file

@ -6,7 +6,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/volume"
portainer "github.com/portainer/portainer/api"
@ -117,12 +116,12 @@ func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) *httperror.H
return err
}
networks, err := cli.NetworkList(r.Context(), network.ListOptions{})
networks, err := cli.NetworkList(r.Context(), types.NetworkListOptions{})
if err != nil {
return httperror.InternalServerError("Unable to retrieve Docker networks", err)
}
networks, err = utils.FilterByResourceControl(tx, networks, portainer.NetworkResourceControl, context, func(c network.Summary) string {
networks, err = utils.FilterByResourceControl(tx, networks, portainer.NetworkResourceControl, context, func(c types.NetworkResource) string {
return c.Name
})
if err != nil {

View file

@ -4,7 +4,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/roar"
)
type endpointSetType map[portainer.EndpointID]bool
@ -50,29 +49,22 @@ func GetEndpointsByTags(tx dataservices.DataStoreTx, tagIDs []portainer.TagID, p
return results, nil
}
func getTrustedEndpoints(tx dataservices.DataStoreTx, endpointIDs roar.Roar[portainer.EndpointID]) ([]portainer.EndpointID, error) {
var innerErr error
func getTrustedEndpoints(tx dataservices.DataStoreTx, endpointIDs []portainer.EndpointID) ([]portainer.EndpointID, error) {
results := []portainer.EndpointID{}
endpointIDs.Iterate(func(endpointID portainer.EndpointID) bool {
for _, endpointID := range endpointIDs {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
innerErr = err
return false
return nil, err
}
if !endpoint.UserTrusted {
return true
continue
}
results = append(results, endpoint.ID)
}
return true
})
return results, innerErr
return results, nil
}
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {

View file

@ -7,7 +7,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/roar"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
)
@ -53,7 +52,6 @@ func calculateEndpointsOrTags(tx dataservices.DataStoreTx, edgeGroup *portainer.
}
edgeGroup.Endpoints = endpointIDs
edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs)
return nil
}
@ -96,7 +94,6 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
Dynamic: payload.Dynamic,
TagIDs: []portainer.TagID{},
Endpoints: []portainer.EndpointID{},
EndpointIDs: roar.Roar[portainer.EndpointID]{},
PartialMatch: payload.PartialMatch,
}
@ -111,5 +108,5 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
return nil
})
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
return txResponse(w, edgeGroup, err)
}

View file

@ -1,62 +0,0 @@
package edgegroups
import (
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
func TestEdgeGroupCreateHandler(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
ID: 1,
Name: "Test Group",
})
require.NoError(t, err)
for i := range 3 {
err = store.Endpoint().Create(&portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "Test Endpoint " + strconv.Itoa(i+1),
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: 1,
})
require.NoError(t, err)
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
EndpointID: portainer.EndpointID(i + 1),
EdgeStacks: map[portainer.EdgeStackID]bool{},
})
require.NoError(t, err)
}
rr := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodPost,
"/edge_groups",
strings.NewReader(`{"Name": "New Edge Group", "Endpoints": [1, 2, 3]}`),
)
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var responseGroup portainer.EdgeGroup
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
require.NoError(t, err)
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
}

View file

@ -5,7 +5,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/roar"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
)
@ -34,9 +33,7 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
return err
})
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
return txResponse(w, edgeGroup, err)
}
func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
@ -53,7 +50,7 @@ func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*porta
return nil, httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err)
}
edgeGroup.EndpointIDs = roar.FromSlice(endpoints)
edgeGroup.Endpoints = endpoints
}
return edgeGroup, err

View file

@ -1,137 +0,0 @@
package edgegroups
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/roar"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEdgeGroupInspectHandler(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
ID: 1,
Name: "Test Group",
})
require.NoError(t, err)
for i := range 3 {
err = store.Endpoint().Create(&portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "Test Endpoint " + strconv.Itoa(i+1),
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: 1,
})
require.NoError(t, err)
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
EndpointID: portainer.EndpointID(i + 1),
EdgeStacks: map[portainer.EdgeStackID]bool{},
})
require.NoError(t, err)
}
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Test Edge Group",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1, 2, 3}),
})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
"/edge_groups/1",
nil,
)
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var responseGroup portainer.EdgeGroup
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
require.NoError(t, err)
assert.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
}
func TestDynamicEdgeGroupInspectHandler(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
ID: 1,
Name: "Test Group",
})
require.NoError(t, err)
err = store.Tag().Create(&portainer.Tag{
ID: 1,
Name: "Test Tag",
Endpoints: map[portainer.EndpointID]bool{
1: true,
2: true,
3: true,
},
})
require.NoError(t, err)
for i := range 3 {
err = store.Endpoint().Create(&portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "Test Endpoint " + strconv.Itoa(i+1),
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: 1,
TagIDs: []portainer.TagID{1},
UserTrusted: true,
})
require.NoError(t, err)
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
EndpointID: portainer.EndpointID(i + 1),
EdgeStacks: map[portainer.EdgeStackID]bool{},
})
require.NoError(t, err)
}
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Test Edge Group",
Dynamic: true,
TagIDs: []portainer.TagID{1},
})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
"/edge_groups/1",
nil,
)
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var responseGroup portainer.EdgeGroup
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
require.NoError(t, err)
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
}

View file

@ -7,17 +7,11 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/roar"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
)
type shadowedEdgeGroup struct {
portainer.EdgeGroup
EndpointIds int `json:"EndpointIds,omitempty"` // Shadow to avoid exposing in the API
}
type decoratedEdgeGroup struct {
shadowedEdgeGroup
portainer.EdgeGroup
HasEdgeStack bool `json:"HasEdgeStack"`
HasEdgeJob bool `json:"HasEdgeJob"`
EndpointTypes []portainer.EndpointType
@ -82,8 +76,8 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
}
edgeGroup := decoratedEdgeGroup{
shadowedEdgeGroup: shadowedEdgeGroup{EdgeGroup: orgEdgeGroup},
EndpointTypes: []portainer.EndpointType{},
EdgeGroup: orgEdgeGroup,
EndpointTypes: []portainer.EndpointType{},
}
if edgeGroup.Dynamic {
endpointIDs, err := GetEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch)
@ -94,16 +88,15 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
edgeGroup.Endpoints = endpointIDs
edgeGroup.TrustedEndpoints = endpointIDs
} else {
trustedEndpoints, err := getTrustedEndpoints(tx, edgeGroup.EndpointIDs)
trustedEndpoints, err := getTrustedEndpoints(tx, edgeGroup.Endpoints)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve environments for Edge group", err)
}
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
edgeGroup.TrustedEndpoints = trustedEndpoints
}
endpointTypes, err := getEndpointTypes(tx, edgeGroup.EndpointIDs)
endpointTypes, err := getEndpointTypes(tx, edgeGroup.Endpoints)
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve environment types for Edge group", err)
}
@ -118,26 +111,15 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
return decoratedEdgeGroups, nil
}
func getEndpointTypes(tx dataservices.DataStoreTx, endpointIds roar.Roar[portainer.EndpointID]) ([]portainer.EndpointType, error) {
var innerErr error
func getEndpointTypes(tx dataservices.DataStoreTx, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) {
typeSet := map[portainer.EndpointType]bool{}
endpointIds.Iterate(func(endpointID portainer.EndpointID) bool {
for _, endpointID := range endpointIds {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil {
innerErr = fmt.Errorf("failed fetching environment: %w", err)
return false
return nil, fmt.Errorf("failed fetching environment: %w", err)
}
typeSet[endpoint.Type] = true
return true
})
if innerErr != nil {
return nil, innerErr
}
endpointTypes := make([]portainer.EndpointType, 0, len(typeSet))

View file

@ -1,19 +1,11 @@
package edgegroups
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/roar"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_getEndpointTypes(t *testing.T) {
@ -46,7 +38,7 @@ func Test_getEndpointTypes(t *testing.T) {
}
for _, test := range tests {
ans, err := getEndpointTypes(datastore, roar.FromSlice(test.endpointIds))
ans, err := getEndpointTypes(datastore, test.endpointIds)
assert.NoError(t, err, "getEndpointTypes shouldn't fail")
assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
@ -56,61 +48,6 @@ func Test_getEndpointTypes(t *testing.T) {
func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) {
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
_, err := getEndpointTypes(datastore, roar.FromSlice([]portainer.EndpointID{1}))
_, err := getEndpointTypes(datastore, []portainer.EndpointID{1})
assert.Error(t, err, "getEndpointTypes should fail")
}
func TestEdgeGroupListHandler(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
ID: 1,
Name: "Test Group",
})
require.NoError(t, err)
for i := range 3 {
err = store.Endpoint().Create(&portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "Test Endpoint " + strconv.Itoa(i+1),
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: 1,
})
require.NoError(t, err)
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
EndpointID: portainer.EndpointID(i + 1),
EdgeStacks: map[portainer.EdgeStackID]bool{},
})
require.NoError(t, err)
}
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Test Edge Group",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1, 2, 3}),
})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
"/edge_groups",
nil,
)
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var responseGroups []decoratedEdgeGroup
err = json.NewDecoder(rr.Body).Decode(&responseGroups)
require.NoError(t, err)
require.Len(t, responseGroups, 1)
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroups[0].Endpoints)
require.Len(t, responseGroups[0].TrustedEndpoints, 0)
}

View file

@ -158,12 +158,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
return nil
})
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
return txResponse(w, edgeGroup, err)
}
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return err
}
@ -179,6 +179,12 @@ func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoi
edgeStackSet[edgeStackID] = true
}
if relation == nil {
relation = &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: make(map[portainer.EdgeStackID]bool),
}
}
relation.EdgeStacks = edgeStackSet
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)

View file

@ -1,70 +0,0 @@
package edgegroups
import (
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/roar"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
func TestEdgeGroupUpdateHandler(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
handler := NewHandler(testhelpers.NewTestRequestBouncer())
handler.DataStore = store
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
ID: 1,
Name: "Test Group",
})
require.NoError(t, err)
for i := range 3 {
err = store.Endpoint().Create(&portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "Test Endpoint " + strconv.Itoa(i+1),
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: 1,
})
require.NoError(t, err)
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
EndpointID: portainer.EndpointID(i + 1),
EdgeStacks: map[portainer.EdgeStackID]bool{},
})
require.NoError(t, err)
}
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Test Edge Group",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
})
require.NoError(t, err)
rr := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodPut,
"/edge_groups/1",
strings.NewReader(`{"Endpoints": [1, 2, 3]}`),
)
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
var responseGroup portainer.EdgeGroup
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
require.NoError(t, err)
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
}

View file

@ -101,7 +101,8 @@ func (payload *edgeStackFromFileUploadPayload) Validate(r *http.Request) error {
// @router /edge_stacks/create/file [post]
func (handler *Handler) createEdgeStackFromFileUpload(r *http.Request, tx dataservices.DataStoreTx, dryrun bool) (*portainer.EdgeStack, error) {
payload := &edgeStackFromFileUploadPayload{}
if err := payload.Validate(r); err != nil {
err := payload.Validate(r)
if err != nil {
return nil, err
}

View file

@ -33,8 +33,6 @@ type edgeStackFromGitRepositoryPayload struct {
RepositoryUsername string `example:"myGitUsername"`
// Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string `example:"myGitPassword"`
// RepositoryAuthorizationType is the authorization type to use
RepositoryAuthorizationType gittypes.GitCredentialAuthType `example:"0"`
// Path to the Stack file inside the Git repository
FilePathInRepository string `example:"docker-compose.yml" default:"docker-compose.yml"`
// List of identifiers of EdgeGroups
@ -105,7 +103,8 @@ func (payload *edgeStackFromGitRepositoryPayload) Validate(r *http.Request) erro
// @router /edge_stacks/create/repository [post]
func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dataservices.DataStoreTx, dryrun bool, userID portainer.UserID) (*portainer.EdgeStack, error) {
var payload edgeStackFromGitRepositoryPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
@ -127,9 +126,8 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
if payload.RepositoryAuthentication {
repoConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
AuthorizationType: payload.RepositoryAuthorizationType,
Username: payload.RepositoryUsername,
Password: payload.RepositoryPassword,
}
}
@ -139,31 +137,24 @@ func (handler *Handler) createEdgeStackFromGitRepository(r *http.Request, tx dat
}
func (handler *Handler) storeManifestFromGitRepository(tx dataservices.DataStoreTx, stackFolder string, relatedEndpointIds []portainer.EndpointID, deploymentType portainer.EdgeStackDeploymentType, currentUserID portainer.UserID, repositoryConfig gittypes.RepoConfig) (composePath, manifestPath, projectPath string, err error) {
if hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType); err != nil {
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType)
if err != nil {
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
} else if hasWrongType {
}
if hasWrongType {
return "", "", "", errors.New("edge stack with config do not match the environment type")
}
projectPath = handler.FileService.GetEdgeStackProjectPath(stackFolder)
repositoryUsername := ""
repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if repositoryConfig.Authentication != nil && repositoryConfig.Authentication.Password != "" {
repositoryUsername = repositoryConfig.Authentication.Username
repositoryPassword = repositoryConfig.Authentication.Password
repositoryAuthType = repositoryConfig.Authentication.AuthorizationType
}
if err := handler.GitService.CloneRepository(
projectPath,
repositoryConfig.URL,
repositoryConfig.ReferenceName,
repositoryUsername,
repositoryPassword,
repositoryAuthType,
repositoryConfig.TLSSkipVerify,
); err != nil {
err = handler.GitService.CloneRepository(projectPath, repositoryConfig.URL, repositoryConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryConfig.TLSSkipVerify)
if err != nil {
return "", "", "", err
}

View file

@ -76,7 +76,8 @@ func (payload *edgeStackFromStringPayload) Validate(r *http.Request) error {
// @router /edge_stacks/create/string [post]
func (handler *Handler) createEdgeStackFromFileContent(r *http.Request, tx dataservices.DataStoreTx, dryrun bool) (*portainer.EdgeStack, error) {
var payload edgeStackFromStringPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
@ -95,9 +96,11 @@ func (handler *Handler) createEdgeStackFromFileContent(r *http.Request, tx datas
}
func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolder string, deploymentType portainer.EdgeStackDeploymentType, relatedEndpointIds []portainer.EndpointID, fileContent []byte) (composePath, manifestPath, projectPath string, err error) {
if hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType); err != nil {
hasWrongType, err := hasWrongEnvironmentType(tx.Endpoint(), relatedEndpointIds, deploymentType)
if err != nil {
return "", "", "", fmt.Errorf("unable to check for existence of non fitting environments: %w", err)
} else if hasWrongType {
}
if hasWrongType {
return "", "", "", errors.New("edge stack with config do not match the environment type")
}
@ -121,6 +124,7 @@ func (handler *Handler) storeFileContent(tx dataservices.DataStoreTx, stackFolde
}
return "", manifestPath, projectPath, nil
}
errMessage := fmt.Sprintf("invalid deployment type: %d", deploymentType)

View file

@ -8,10 +8,8 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/roar"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
// Create
@ -25,12 +23,14 @@ func TestCreateAndInspect(t *testing.T) {
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpoint.ID}),
Endpoints: []portainer.EndpointID{endpoint.ID},
PartialMatch: false,
}
err := handler.DataStore.EdgeGroup().Create(&edgeGroup)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpoint.ID,
@ -38,7 +38,9 @@ func TestCreateAndInspect(t *testing.T) {
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
payload := edgeStackFromStringPayload{
Name: "test-stack",
@ -48,14 +50,16 @@ func TestCreateAndInspect(t *testing.T) {
}
jsonPayload, err := json.Marshal(payload)
require.NoError(t, err)
if err != nil {
t.Fatal("JSON marshal error:", err)
}
r := bytes.NewBuffer(jsonPayload)
// Create EdgeStack
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", r)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@ -66,11 +70,15 @@ func TestCreateAndInspect(t *testing.T) {
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
require.NoError(t, err)
if err != nil {
t.Fatal("error decoding response:", err)
}
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", data.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
@ -82,7 +90,9 @@ func TestCreateAndInspect(t *testing.T) {
data = portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
require.NoError(t, err)
if err != nil {
t.Fatal("error decoding response:", err)
}
if payload.Name != data.Name {
t.Fatalf("expected EdgeStack Name %s, found %s", payload.Name, data.Name)

View file

@ -30,9 +30,10 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request)
return httperror.BadRequest("Invalid edge stack identifier route variable", err)
}
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return handler.deleteEdgeStack(tx, portainer.EdgeStackID(edgeStackID))
}); err != nil {
})
if err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr

View file

@ -8,10 +8,9 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Delete
@ -24,7 +23,9 @@ func TestDeleteAndInspect(t *testing.T) {
// Inspect
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
@ -36,7 +37,9 @@ func TestDeleteAndInspect(t *testing.T) {
data := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&data)
require.NoError(t, err)
if err != nil {
t.Fatal("error decoding response:", err)
}
if data.ID != edgeStack.ID {
t.Fatalf("expected EdgeStackID %d, found %d", int(edgeStack.ID), data.ID)
@ -44,7 +47,9 @@ func TestDeleteAndInspect(t *testing.T) {
// Delete
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
@ -56,7 +61,9 @@ func TestDeleteAndInspect(t *testing.T) {
// Inspect
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
@ -110,12 +117,15 @@ func TestDeleteEdgeStack_RemoveProjectFolder(t *testing.T) {
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(payload)
require.NoError(t, err)
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
t.Fatal("error encoding payload:", err)
}
// Create
req, err := http.NewRequest(http.MethodPost, "/edge_stacks/create/string", &buf)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
@ -128,8 +138,9 @@ func TestDeleteEdgeStack_RemoveProjectFolder(t *testing.T) {
assert.DirExists(t, handler.FileService.GetEdgeStackProjectPath("1"))
// Delete
req, err = http.NewRequest(http.MethodDelete, "/edge_stacks/1", nil)
require.NoError(t, err)
if req, err = http.NewRequest(http.MethodDelete, "/edge_stacks/1", nil); err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()

View file

@ -4,7 +4,6 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
@ -34,35 +33,5 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request)
return handlerDBErr(err, "Unable to find an edge stack with the specified identifier inside the database")
}
if err := fillEdgeStackStatus(handler.DataStore, edgeStack); err != nil {
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
}
return response.JSON(w, edgeStack)
}
func fillEdgeStackStatus(tx dataservices.DataStoreTx, edgeStack *portainer.EdgeStack) error {
status, err := tx.EdgeStackStatus().ReadAll(edgeStack.ID)
if err != nil {
return err
}
edgeStack.Status = make(map[portainer.EndpointID]portainer.EdgeStackStatus, len(status))
emptyStatus := make([]portainer.EdgeStackDeploymentStatus, 0)
for _, s := range status {
if s.Status == nil {
s.Status = emptyStatus
}
edgeStack.Status[s.EndpointID] = portainer.EdgeStackStatus{
Status: s.Status,
EndpointID: s.EndpointID,
DeploymentInfo: s.DeploymentInfo,
ReadyRePullImage: s.ReadyRePullImage,
}
}
return nil
}

View file

@ -3,39 +3,10 @@ package edgestacks
import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/slicesx"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
type aggregatedStatusesMap map[portainer.EdgeStackStatusType]int
type SummarizedStatus string
const (
sumStatusUnavailable SummarizedStatus = "Unavailable"
sumStatusDeploying SummarizedStatus = "Deploying"
sumStatusFailed SummarizedStatus = "Failed"
sumStatusPaused SummarizedStatus = "Paused"
sumStatusPartiallyRunning SummarizedStatus = "PartiallyRunning"
sumStatusCompleted SummarizedStatus = "Completed"
sumStatusRunning SummarizedStatus = "Running"
)
type edgeStackStatusSummary struct {
AggregatedStatus aggregatedStatusesMap
Status SummarizedStatus
Reason string
}
type edgeStackListResponseItem struct {
portainer.EdgeStack
StatusSummary edgeStackStatusSummary
}
// @id EdgeStackList
// @summary Fetches the list of EdgeStacks
// @description **Access policy**: administrator
@ -43,122 +14,16 @@ type edgeStackListResponseItem struct {
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param summarizeStatuses query boolean false "will summarize the statuses"
// @success 200 {array} portainer.EdgeStack
// @failure 500
// @failure 400
// @failure 503 "Edge compute features are disabled"
// @router /edge_stacks [get]
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
summarizeStatuses, _ := request.RetrieveBooleanQueryParameter(r, "summarizeStatuses", true)
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
if err != nil {
return httperror.InternalServerError("Unable to retrieve edge stacks from the database", err)
}
res := make([]edgeStackListResponseItem, len(edgeStacks))
for i := range edgeStacks {
res[i].EdgeStack = edgeStacks[i]
if summarizeStatuses {
if err := fillStatusSummary(handler.DataStore, &res[i]); err != nil {
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
}
} else if err := fillEdgeStackStatus(handler.DataStore, &res[i].EdgeStack); err != nil {
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
}
}
return response.JSON(w, res)
}
func fillStatusSummary(tx dataservices.DataStoreTx, edgeStack *edgeStackListResponseItem) error {
statuses, err := tx.EdgeStackStatus().ReadAll(edgeStack.ID)
if err != nil {
return err
}
aggregated := make(aggregatedStatusesMap)
for _, envStatus := range statuses {
for _, status := range envStatus.Status {
aggregated[status.Type]++
}
}
status, reason := SummarizeStatuses(statuses, edgeStack.NumDeployments)
edgeStack.StatusSummary = edgeStackStatusSummary{
AggregatedStatus: aggregated,
Status: status,
Reason: reason,
}
edgeStack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
return nil
}
func SummarizeStatuses(statuses []portainer.EdgeStackStatusForEnv, numDeployments int) (SummarizedStatus, string) {
if numDeployments == 0 {
return sumStatusUnavailable, "Your edge stack is currently unavailable due to the absence of an available environment in your edge group"
}
allStatuses := slicesx.FlatMap(statuses, func(x portainer.EdgeStackStatusForEnv) []portainer.EdgeStackDeploymentStatus {
return x.Status
})
lastStatuses := slicesx.Map(
slicesx.Filter(
statuses,
func(s portainer.EdgeStackStatusForEnv) bool {
return len(s.Status) > 0
},
),
func(x portainer.EdgeStackStatusForEnv) portainer.EdgeStackDeploymentStatus {
return x.Status[len(x.Status)-1]
},
)
if len(lastStatuses) == 0 {
return sumStatusDeploying, ""
}
if allFailed := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
return s.Type == portainer.EdgeStackStatusError
}); allFailed {
return sumStatusFailed, ""
}
if hasPaused := slicesx.Some(allStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
return s.Type == portainer.EdgeStackStatusPausedDeploying
}); hasPaused {
return sumStatusPaused, ""
}
if len(lastStatuses) < numDeployments {
return sumStatusDeploying, ""
}
hasDeploying := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusDeploying })
hasRunning := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusRunning })
hasFailed := slicesx.Some(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusError })
if hasRunning && hasFailed && !hasDeploying {
return sumStatusPartiallyRunning, ""
}
if allCompleted := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool { return s.Type == portainer.EdgeStackStatusCompleted }); allCompleted {
return sumStatusCompleted, ""
}
if allRunning := slicesx.Every(lastStatuses, func(s portainer.EdgeStackDeploymentStatus) bool {
return s.Type == portainer.EdgeStackStatusRunning
}); allRunning {
return sumStatusRunning, ""
}
return sumStatusDeploying, ""
return response.JSON(w, edgeStacks)
}

View file

@ -9,10 +9,11 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
type updateStatusPayload struct {
@ -77,25 +78,12 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
}
var stack *portainer.EdgeStack
updateFn := func(stack *portainer.EdgeStack) (*portainer.EdgeStack, error) {
return handler.updateEdgeStackStatus(stack, stack.ID, payload)
}
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
var err error
stack, err = tx.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID))
if err != nil {
if dataservices.IsErrObjectNotFound(err) {
return nil
}
return httperror.InternalServerError("Unable to retrieve Edge stack from the database", err)
}
if err := handler.updateEdgeStackStatus(tx, stack, stack.ID, payload); err != nil {
return httperror.InternalServerError("Unable to update Edge stack status", err)
}
return nil
}); err != nil {
stack, err := handler.stackCoordinator.UpdateStatus(r, portainer.EdgeStackID(stackID), updateFn)
if err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
@ -108,36 +96,43 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return nil
}
if err := fillEdgeStackStatus(handler.DataStore, stack); err != nil {
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
}
return response.JSON(w, stack)
}
func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, stack *portainer.EdgeStack, stackID portainer.EdgeStackID, payload updateStatusPayload) error {
func (handler *Handler) updateEdgeStackStatus(stack *portainer.EdgeStack, stackID portainer.EdgeStackID, payload updateStatusPayload) (*portainer.EdgeStack, error) {
if payload.Version > 0 && payload.Version < stack.Version {
return nil
return stack, nil
}
status := *payload.Status
log.Debug().
Int("stackID", int(stackID)).
Int("status", int(status)).
Msg("Updating stack status")
deploymentStatus := portainer.EdgeStackDeploymentStatus{
Type: status,
Error: payload.Error,
Time: payload.Time,
}
updateEnvStatus(payload.EndpointID, stack, deploymentStatus)
return stack, nil
}
func updateEnvStatus(environmentId portainer.EndpointID, stack *portainer.EdgeStack, deploymentStatus portainer.EdgeStackDeploymentStatus) {
if deploymentStatus.Type == portainer.EdgeStackStatusRemoved {
return tx.EdgeStackStatus().Delete(stackID, payload.EndpointID)
delete(stack.Status, environmentId)
return
}
environmentStatus, err := tx.EdgeStackStatus().Read(stackID, payload.EndpointID)
if err != nil && !tx.IsErrObjectNotFound(err) {
return err
} else if tx.IsErrObjectNotFound(err) {
environmentStatus = &portainer.EdgeStackStatusForEnv{
EndpointID: payload.EndpointID,
environmentStatus, ok := stack.Status[environmentId]
if !ok {
environmentStatus = portainer.EdgeStackStatus{
EndpointID: environmentId,
Status: []portainer.EdgeStackDeploymentStatus{},
}
}
@ -148,5 +143,5 @@ func (handler *Handler) updateEdgeStackStatus(tx dataservices.DataStoreTx, stack
environmentStatus.Status = append(environmentStatus.Status, deploymentStatus)
}
return tx.EdgeStackStatus().Update(stackID, payload.EndpointID, environmentStatus)
stack.Status[environmentId] = environmentStatus
}

View file

@ -0,0 +1,155 @@
package edgestacks
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
type statusRequest struct {
respCh chan statusResponse
stackID portainer.EdgeStackID
updateFn statusUpdateFn
}
type statusResponse struct {
Stack *portainer.EdgeStack
Error error
}
type statusUpdateFn func(*portainer.EdgeStack) (*portainer.EdgeStack, error)
type EdgeStackStatusUpdateCoordinator struct {
updateCh chan statusRequest
dataStore dataservices.DataStore
}
var errAnotherStackUpdateInProgress = errors.New("another stack update is in progress")
func NewEdgeStackStatusUpdateCoordinator(dataStore dataservices.DataStore) *EdgeStackStatusUpdateCoordinator {
return &EdgeStackStatusUpdateCoordinator{
updateCh: make(chan statusRequest),
dataStore: dataStore,
}
}
func (c *EdgeStackStatusUpdateCoordinator) Start() {
for {
c.loop()
}
}
func (c *EdgeStackStatusUpdateCoordinator) loop() {
u := <-c.updateCh
respChs := []chan statusResponse{u.respCh}
var stack *portainer.EdgeStack
err := c.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
// 1. Load the edge stack
var err error
stack, err = loadEdgeStack(tx, u.stackID)
if err != nil {
return err
}
// Return early when the agent tries to update the status on a deleted stack
if stack == nil {
return nil
}
// 2. Mutate the edge stack opportunistically until there are no more pending updates
for {
stack, err = u.updateFn(stack)
if err != nil {
return err
}
if m, ok := c.getNextUpdate(stack.ID); ok {
u = m
} else {
break
}
respChs = append(respChs, u.respCh)
}
// 3. Save the changes back to the database
if err := tx.EdgeStack().UpdateEdgeStack(stack.ID, stack); err != nil {
return handlerDBErr(fmt.Errorf("unable to update Edge stack: %w.", err), "Unable to persist the stack changes inside the database")
}
return nil
})
// 4. Send back the responses
for _, ch := range respChs {
ch <- statusResponse{Stack: stack, Error: err}
}
}
func loadEdgeStack(tx dataservices.DataStoreTx, stackID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
stack, err := tx.EdgeStack().EdgeStack(stackID)
if err != nil {
if dataservices.IsErrObjectNotFound(err) {
// Skip the error when the agent tries to update the status on a deleted stack
log.Debug().
Err(err).
Int("stackID", int(stackID)).
Msg("Unable to find a stack inside the database, skipping error")
return nil, nil
}
return nil, fmt.Errorf("unable to retrieve Edge stack from the database: %w.", err)
}
return stack, nil
}
func (c *EdgeStackStatusUpdateCoordinator) getNextUpdate(stackID portainer.EdgeStackID) (statusRequest, bool) {
for {
select {
case u := <-c.updateCh:
// Discard the update and let the agent retry
if u.stackID != stackID {
u.respCh <- statusResponse{Error: errAnotherStackUpdateInProgress}
continue
}
return u, true
default:
return statusRequest{}, false
}
}
}
func (c *EdgeStackStatusUpdateCoordinator) UpdateStatus(r *http.Request, stackID portainer.EdgeStackID, updateFn statusUpdateFn) (*portainer.EdgeStack, error) {
respCh := make(chan statusResponse)
defer close(respCh)
msg := statusRequest{
respCh: respCh,
stackID: stackID,
updateFn: updateFn,
}
select {
case c.updateCh <- msg:
r := <-respCh
return r.Stack, r.Error
case <-r.Context().Done():
return nil, r.Context().Err()
}
}

View file

@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
// Update Status
@ -29,11 +28,15 @@ func TestUpdateStatusAndInspect(t *testing.T) {
}
jsonPayload, err := json.Marshal(payload)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
@ -45,7 +48,9 @@ func TestUpdateStatusAndInspect(t *testing.T) {
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
@ -57,10 +62,14 @@ func TestUpdateStatusAndInspect(t *testing.T) {
updatedStack := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
require.NoError(t, err)
if err != nil {
t.Fatal("error decoding response:", err)
}
endpointStatus, ok := updatedStack.Status[payload.EndpointID]
require.True(t, ok)
if !ok {
t.Fatal("Missing status")
}
lastStatus := endpointStatus.Status[len(endpointStatus.Status)-1]
@ -75,8 +84,8 @@ func TestUpdateStatusAndInspect(t *testing.T) {
if endpointStatus.EndpointID != payload.EndpointID {
t.Fatalf("expected EndpointID %d, found %d", payload.EndpointID, endpointStatus.EndpointID)
}
}
}
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
handler, _ := setupHandler(t)
@ -127,11 +136,15 @@ func TestUpdateStatusWithInvalidPayload(t *testing.T) {
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()

View file

@ -15,10 +15,8 @@ import (
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/roar"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
// Helpers
@ -53,21 +51,27 @@ func setupHandler(t *testing.T) (*Handler, string) {
t.Fatal(err)
}
coord := NewEdgeStackStatusUpdateCoordinator(store)
go coord.Start()
handler := NewHandler(
security.NewRequestBouncer(store, jwtService, apiKeyService),
store,
edgestacks.NewService(store),
coord,
)
handler.FileService = fs
settings, err := handler.DataStore.Settings().Settings()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
settings.EnableEdgeComputeFeatures = true
err = handler.DataStore.Settings().UpdateSettings(settings)
require.NoError(t, err)
if err := handler.DataStore.Settings().UpdateSettings(settings); err != nil {
t.Fatal(err)
}
handler.GitService = testhelpers.NewGitService(errors.New("Clone error"), "git-service-id")
@ -86,8 +90,9 @@ func createEndpointWithId(t *testing.T, store dataservices.DataStore, endpointID
LastCheckInDate: time.Now().Unix(),
}
err := store.Endpoint().Create(&endpoint)
require.NoError(t, err)
if err := store.Endpoint().Create(&endpoint); err != nil {
t.Fatal(err)
}
return endpoint
}
@ -104,17 +109,19 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
Name: "EdgeGroup 1",
Dynamic: false,
TagIDs: nil,
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpointID}),
Endpoints: []portainer.EndpointID{endpointID},
PartialMatch: false,
}
err := store.EdgeGroup().Create(&edgeGroup)
require.NoError(t, err)
if err := store.EdgeGroup().Create(&edgeGroup); err != nil {
t.Fatal(err)
}
edgeStackID := portainer.EdgeStackID(14)
edgeStack := portainer.EdgeStack{
ID: edgeStackID,
Name: "test-edge-stack-" + strconv.Itoa(int(edgeStackID)),
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{},
CreationDate: time.Now().Unix(),
EdgeGroups: []portainer.EdgeGroupID{edgeGroup.ID},
ProjectPath: "/project/path",
@ -131,11 +138,13 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
},
}
err = store.EdgeStack().Create(edgeStack.ID, &edgeStack)
require.NoError(t, err)
if err := store.EdgeStack().Create(edgeStack.ID, &edgeStack); err != nil {
t.Fatal(err)
}
err = store.EndpointRelation().Create(&endpointRelation)
require.NoError(t, err)
if err := store.EndpointRelation().Create(&endpointRelation); err != nil {
t.Fatal(err)
}
return edgeStack
}
@ -146,8 +155,8 @@ func createEdgeGroup(t *testing.T, store dataservices.DataStore) portainer.EdgeG
Name: "EdgeGroup 1",
}
err := store.EdgeGroup().Create(&edgeGroup)
require.NoError(t, err)
if err := store.EdgeGroup().Create(&edgeGroup); err != nil {
t.Fatal(err)
}
return edgeGroup
}

View file

@ -74,10 +74,6 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unexpected error", err)
}
if err := fillEdgeStackStatus(handler.DataStore, stack); err != nil {
return handlerDBErr(err, "Unable to retrieve edge stack status from the database")
}
return response.JSON(w, stack)
}
@ -124,7 +120,7 @@ func (handler *Handler) updateEdgeStack(tx dataservices.DataStoreTx, stackID por
stack.EdgeGroups = groupsIds
if payload.UpdateVersion {
if err := handler.updateStackVersion(tx, stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
if err := handler.updateStackVersion(stack, payload.DeploymentType, []byte(payload.StackFileContent), "", relatedEndpointIds); err != nil {
return nil, httperror.InternalServerError("Unable to update stack version", err)
}
}

View file

@ -9,10 +9,9 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/roar"
"github.com/stretchr/testify/require"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/require"
)
// Update
@ -26,8 +25,9 @@ func TestUpdateAndInspect(t *testing.T) {
endpointID := portainer.EndpointID(6)
newEndpoint := createEndpointWithId(t, handler.DataStore, endpointID)
err := handler.DataStore.Endpoint().Create(&newEndpoint)
require.NoError(t, err)
if err := handler.DataStore.Endpoint().Create(&newEndpoint); err != nil {
t.Fatal(err)
}
endpointRelation := portainer.EndpointRelation{
EndpointID: endpointID,
@ -36,20 +36,22 @@ func TestUpdateAndInspect(t *testing.T) {
},
}
err = handler.DataStore.EndpointRelation().Create(&endpointRelation)
require.NoError(t, err)
if err := handler.DataStore.EndpointRelation().Create(&endpointRelation); err != nil {
t.Fatal(err)
}
newEdgeGroup := portainer.EdgeGroup{
ID: 2,
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
EndpointIDs: roar.FromSlice([]portainer.EndpointID{newEndpoint.ID}),
Endpoints: []portainer.EndpointID{newEndpoint.ID},
PartialMatch: false,
}
err = handler.DataStore.EdgeGroup().Create(&newEdgeGroup)
require.NoError(t, err)
if err := handler.DataStore.EdgeGroup().Create(&newEdgeGroup); err != nil {
t.Fatal(err)
}
payload := updateEdgeStackPayload{
StackFileContent: "update-test",
@ -59,11 +61,15 @@ func TestUpdateAndInspect(t *testing.T) {
}
jsonPayload, err := json.Marshal(payload)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()
@ -75,7 +81,9 @@ func TestUpdateAndInspect(t *testing.T) {
// Get updated edge stack
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), nil)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec = httptest.NewRecorder()
@ -86,8 +94,9 @@ func TestUpdateAndInspect(t *testing.T) {
}
updatedStack := portainer.EdgeStack{}
err = json.NewDecoder(rec.Body).Decode(&updatedStack)
require.NoError(t, err)
if err := json.NewDecoder(rec.Body).Decode(&updatedStack); err != nil {
t.Fatal("error decoding response:", err)
}
if payload.UpdateVersion && updatedStack.Version != edgeStack.Version+1 {
t.Fatalf("expected EdgeStack version %d, found %d", edgeStack.Version+1, updatedStack.Version+1)
@ -113,7 +122,7 @@ func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
Name: "EdgeGroup 2",
Dynamic: false,
TagIDs: nil,
EndpointIDs: roar.FromSlice([]portainer.EndpointID{8889}),
Endpoints: []portainer.EndpointID{8889},
PartialMatch: false,
}
@ -217,11 +226,15 @@ func TestUpdateWithInvalidPayload(t *testing.T) {
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
jsonPayload, err := json.Marshal(tc.Payload)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d", edgeStack.ID), r)
require.NoError(t, err)
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Add("x-api-key", rawAPIKey)
rec := httptest.NewRecorder()

View file

@ -22,15 +22,17 @@ type Handler struct {
GitService portainer.GitService
edgeStacksService *edgestackservice.Service
KubernetesDeployer portainer.KubernetesDeployer
stackCoordinator *EdgeStackStatusUpdateCoordinator
}
// NewHandler creates a handler to manage environment(endpoint) group operations.
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, edgeStacksService *edgestackservice.Service) *Handler {
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, edgeStacksService *edgestackservice.Service, stackCoordinator *EdgeStackStatusUpdateCoordinator) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
DataStore: dataStore,
edgeStacksService: edgeStacksService,
stackCoordinator: stackCoordinator,
}
h.Handle("/edge_stacks/create/{method}",

View file

@ -5,18 +5,15 @@ import (
"strconv"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
edgestackutils "github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/rs/zerolog/log"
)
func (handler *Handler) updateStackVersion(tx dataservices.DataStoreTx, stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte, oldGitHash string, relatedEnvironmentsIDs []portainer.EndpointID) error {
stack.Version++
if err := tx.EdgeStackStatus().Clear(stack.ID, relatedEnvironmentsIDs); err != nil {
return err
}
func (handler *Handler) updateStackVersion(stack *portainer.EdgeStack, deploymentType portainer.EdgeStackDeploymentType, config []byte, oldGitHash string, relatedEnvironmentsIDs []portainer.EndpointID) error {
stack.Version = stack.Version + 1
stack.Status = edgestackutils.NewStatus(stack.Status, relatedEnvironmentsIDs)
return handler.storeStackFile(stack, deploymentType, config)
}

View file

@ -264,6 +264,9 @@ func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID p
func (handler *Handler) buildEdgeStacks(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) ([]stackStatusResponse, *httperror.HandlerError) {
relation, err := tx.EndpointRelation().EndpointRelation(endpointID)
if err != nil {
if tx.IsErrObjectNotFound(err) {
return nil, nil
}
return nil, httperror.InternalServerError("Unable to retrieve relation object from the database", err)
}

View file

@ -16,7 +16,6 @@ import (
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/roar"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
@ -288,8 +287,11 @@ func TestEdgeStackStatus(t *testing.T) {
edgeStackID := portainer.EdgeStackID(17)
edgeStack := portainer.EdgeStack{
ID: edgeStackID,
Name: "test-edge-stack-17",
ID: edgeStackID,
Name: "test-edge-stack-17",
Status: map[portainer.EndpointID]portainer.EdgeStackStatus{
endpointID: {},
},
CreationDate: time.Now().Unix(),
EdgeGroups: []portainer.EdgeGroupID{1, 2},
ProjectPath: "/project/path",
@ -367,8 +369,8 @@ func TestEdgeJobsResponse(t *testing.T) {
unrelatedEndpoint := localCreateEndpoint(80, nil)
staticEdgeGroup := portainer.EdgeGroup{
ID: 1,
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpointFromStaticEdgeGroup.ID}),
ID: 1,
Endpoints: []portainer.EndpointID{endpointFromStaticEdgeGroup.ID},
}
err := handler.DataStore.EdgeGroup().Create(&staticEdgeGroup)
require.NoError(t, err)

View file

@ -21,10 +21,17 @@ func (handler *Handler) updateEndpointRelations(tx dataservices.DataStoreTx, end
}
endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
if err != nil && !tx.IsErrObjectNotFound(err) {
return err
}
if endpointRelation == nil {
endpointRelation = &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: make(map[portainer.EdgeStackID]bool),
}
}
edgeGroups, err := tx.EdgeGroup().ReadAll()
if err != nil {
return err

View file

@ -563,10 +563,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(tx dataservices.Data
return err
}
if err := endpointutils.InitializeEdgeEndpointRelation(endpoint, tx); err != nil {
return err
}
for _, tagID := range endpoint.TagIDs {
if err := tx.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) {
tag.Endpoints[endpoint.ID] = true

View file

@ -3,6 +3,7 @@ package endpoints
import (
"errors"
"net/http"
"slices"
"strconv"
portainer "github.com/portainer/portainer/api"
@ -199,7 +200,9 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
}
for _, edgeGroup := range edgeGroups {
edgeGroup.EndpointIDs.Remove(endpoint.ID)
edgeGroup.Endpoints = slices.DeleteFunc(edgeGroup.Endpoints, func(e portainer.EndpointID) bool {
return e == endpoint.ID
})
if err := tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup); err != nil {
log.Warn().Err(err).Msg("Unable to update edge group")
@ -211,9 +214,14 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
log.Warn().Err(err).Msg("Unable to retrieve edge stacks from the database")
}
for _, edgeStack := range edgeStacks {
if err := tx.EdgeStackStatus().Delete(edgeStack.ID, endpoint.ID); err != nil {
log.Warn().Err(err).Msg("Unable to delete edge stack status")
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
if err := tx.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack); err != nil {
log.Warn().Err(err).Msg("Unable to update edge stack")
}
}
}

View file

@ -11,7 +11,6 @@ import (
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/roar"
)
func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
@ -43,9 +42,9 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
}
if err := store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "edgegroup-1",
EndpointIDs: roar.FromSlice(endpointIDs),
ID: 1,
Name: "edgegroup-1",
Endpoints: endpointIDs,
}); err != nil {
t.Fatal("could not create edge group:", err)
}
@ -79,7 +78,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
t.Fatal("could not retrieve the edge group:", err)
}
if edgeGroup.EndpointIDs.Len() > 0 {
if len(edgeGroup.Endpoints) > 0 {
t.Fatal("the edge group is not consistent")
}
}

View file

@ -95,11 +95,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
return httperror.BadRequest("Invalid query parameters", err)
}
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(endpoints, query, endpointGroups, edgeGroups, settings, securityContext)
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, edgeGroups, settings)
if err != nil {
return httperror.InternalServerError("Unable to filter endpoints", err)
}
filteredEndpoints = security.FilterEndpoints(filteredEndpoints, endpointGroups, securityContext)
sortEnvironmentsByField(filteredEndpoints, endpointGroups, getSortKey(sortField), sortOrder == "desc")

View file

@ -75,7 +75,7 @@ func (handler *Handler) listRegistries(tx dataservices.DataStoreTx, r *http.Requ
return nil, httperror.InternalServerError("Unable to retrieve registries from the database", err)
}
registries, handleError := handler.filterRegistriesByAccess(tx, r, registries, endpoint, user, securityContext.UserMemberships)
registries, handleError := handler.filterRegistriesByAccess(r, registries, endpoint, user, securityContext.UserMemberships)
if handleError != nil {
return nil, handleError
}
@ -87,15 +87,15 @@ func (handler *Handler) listRegistries(tx dataservices.DataStoreTx, r *http.Requ
return registries, err
}
func (handler *Handler) filterRegistriesByAccess(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
func (handler *Handler) filterRegistriesByAccess(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
if !endpointutils.IsKubernetesEndpoint(endpoint) {
return security.FilterRegistries(registries, user, memberships, endpoint.ID), nil
}
return handler.filterKubernetesEndpointRegistries(tx, r, registries, endpoint, user, memberships)
return handler.filterKubernetesEndpointRegistries(r, registries, endpoint, user, memberships)
}
func (handler *Handler) filterKubernetesEndpointRegistries(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
func (handler *Handler) filterKubernetesEndpointRegistries(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User, memberships []portainer.TeamMembership) ([]portainer.Registry, *httperror.HandlerError) {
namespaceParam, _ := request.RetrieveQueryParameter(r, "namespace", true)
isAdmin, err := security.IsAdmin(r)
if err != nil {
@ -116,7 +116,7 @@ func (handler *Handler) filterKubernetesEndpointRegistries(tx dataservices.DataS
return registries, nil
}
return handler.filterKubernetesRegistriesByUserRole(tx, r, registries, endpoint, user)
return handler.filterKubernetesRegistriesByUserRole(r, registries, endpoint, user)
}
func (handler *Handler) isNamespaceAuthorized(endpoint *portainer.Endpoint, namespace string, userId portainer.UserID, memberships []portainer.TeamMembership, isAdmin bool) (bool, error) {
@ -169,7 +169,7 @@ func registryAccessPoliciesContainsNamespace(registryAccess portainer.RegistryAc
return false
}
func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.DataStoreTx, r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) {
func (handler *Handler) filterKubernetesRegistriesByUserRole(r *http.Request, registries []portainer.Registry, endpoint *portainer.Endpoint, user *portainer.User) ([]portainer.Registry, *httperror.HandlerError) {
err := handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if errors.Is(err, security.ErrAuthorizationRequired) {
return nil, httperror.Forbidden("User is not authorized", err)
@ -178,7 +178,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.Dat
return nil, httperror.InternalServerError("Unable to retrieve info from request context", err)
}
userNamespaces, err := handler.userNamespaces(tx, endpoint, user)
userNamespaces, err := handler.userNamespaces(endpoint, user)
if err != nil {
return nil, httperror.InternalServerError("unable to retrieve user namespaces", err)
}
@ -186,7 +186,7 @@ func (handler *Handler) filterKubernetesRegistriesByUserRole(tx dataservices.Dat
return filterRegistriesByNamespaces(registries, endpoint.ID, userNamespaces), nil
}
func (handler *Handler) userNamespaces(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
func (handler *Handler) userNamespaces(endpoint *portainer.Endpoint, user *portainer.User) ([]string, error) {
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return nil, err
@ -197,7 +197,7 @@ func (handler *Handler) userNamespaces(tx dataservices.DataStoreTx, endpoint *po
return nil, err
}
userMemberships, err := tx.TeamMembership().TeamMembershipsByUserID(user.ID)
userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return nil, err
}

View file

@ -11,10 +11,9 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/roar"
"github.com/portainer/portainer/api/slicesx"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/pkg/errors"
@ -141,14 +140,11 @@ func (handler *Handler) filterEndpointsByQuery(
groups []portainer.EndpointGroup,
edgeGroups []portainer.EdgeGroup,
settings *portainer.Settings,
context *security.RestrictedRequestContext,
) ([]portainer.Endpoint, int, error) {
totalAvailableEndpoints := len(filteredEndpoints)
if len(query.endpointIds) > 0 {
endpointIDs := roar.FromSlice(query.endpointIds)
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
}
if len(query.excludeIds) > 0 {
@ -185,16 +181,11 @@ func (handler *Handler) filterEndpointsByQuery(
}
// filter edge environments by trusted/untrusted
// only portainer admins are allowed to see untrusted environments
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return true
}
if query.edgeDeviceUntrusted {
return !endpoint.UserTrusted && context.IsAdmin
}
return endpoint.UserTrusted == !query.edgeDeviceUntrusted
})
@ -256,17 +247,19 @@ func (handler *Handler) filterEndpointsByQuery(
return filteredEndpoints, totalAvailableEndpoints, nil
}
func endpointStatusInStackMatchesFilter(stackStatus *portainer.EdgeStackStatusForEnv, envId portainer.EndpointID, statusFilter portainer.EdgeStackStatusType) bool {
func endpointStatusInStackMatchesFilter(edgeStackStatus map[portainer.EndpointID]portainer.EdgeStackStatus, envId portainer.EndpointID, statusFilter portainer.EdgeStackStatusType) bool {
status, ok := edgeStackStatus[envId]
// consider that if the env has no status in the stack it is in Pending state
if statusFilter == portainer.EdgeStackStatusPending {
return stackStatus == nil || len(stackStatus.Status) == 0
return !ok || len(status.Status) == 0
}
if stackStatus == nil {
if !ok {
return false
}
return slices.ContainsFunc(stackStatus.Status, func(s portainer.EdgeStackDeploymentStatus) bool {
return slices.ContainsFunc(status.Status, func(s portainer.EdgeStackDeploymentStatus) bool {
return s.Type == statusFilter
})
}
@ -277,7 +270,7 @@ func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId port
return nil, errors.WithMessage(err, "Unable to retrieve edge stack from the database")
}
envIds := roar.Roar[portainer.EndpointID]{}
envIds := make([]portainer.EndpointID, 0)
for _, edgeGroupdId := range stack.EdgeGroups {
edgeGroup, err := datastore.EdgeGroup().Read(edgeGroupdId)
if err != nil {
@ -289,37 +282,25 @@ func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId port
if err != nil {
return nil, errors.WithMessage(err, "Unable to retrieve environments and environment groups for Edge group")
}
edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs)
edgeGroup.Endpoints = endpointIDs
}
envIds.Union(edgeGroup.EndpointIDs)
envIds = append(envIds, edgeGroup.Endpoints...)
}
if statusFilter != nil {
var innerErr error
envIds.Iterate(func(envId portainer.EndpointID) bool {
edgeStackStatus, err := datastore.EdgeStackStatus().Read(edgeStackId, envId)
if dataservices.IsErrObjectNotFound(err) {
return true
} else if err != nil {
innerErr = errors.WithMessagef(err, "Unable to retrieve edge stack status for environment %d", envId)
return false
n := 0
for _, envId := range envIds {
if endpointStatusInStackMatchesFilter(stack.Status, envId, *statusFilter) {
envIds[n] = envId
n++
}
if !endpointStatusInStackMatchesFilter(edgeStackStatus, portainer.EndpointID(envId), *statusFilter) {
envIds.Remove(envId)
}
return true
})
if innerErr != nil {
return nil, innerErr
}
envIds = envIds[:n]
}
filteredEndpoints := filteredEndpointsByIds(endpoints, envIds)
uniqueIds := slicesx.Unique(envIds)
filteredEndpoints := filteredEndpointsByIds(endpoints, uniqueIds)
return filteredEndpoints, nil
}
@ -351,14 +332,16 @@ func filterEndpointsByEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []
}
edgeGroups = edgeGroups[:n]
endpointIDSet := roar.Roar[portainer.EndpointID]{}
endpointIDSet := make(map[portainer.EndpointID]struct{})
for _, edgeGroup := range edgeGroups {
endpointIDSet.Union(edgeGroup.EndpointIDs)
for _, endpointID := range edgeGroup.Endpoints {
endpointIDSet[endpointID] = struct{}{}
}
}
n = 0
for _, endpoint := range endpoints {
if endpointIDSet.Contains(endpoint.ID) {
if _, exists := endpointIDSet[endpoint.ID]; exists {
endpoints[n] = endpoint
n++
}
@ -374,11 +357,12 @@ func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGr
}
n := 0
excludeEndpointIDSet := roar.Roar[portainer.EndpointID]{}
excludeEndpointIDSet := make(map[portainer.EndpointID]struct{})
for _, edgeGroup := range edgeGroups {
if _, ok := excludeEdgeGroupIDSet[edgeGroup.ID]; ok {
excludeEndpointIDSet.Union(edgeGroup.EndpointIDs)
for _, endpointID := range edgeGroup.Endpoints {
excludeEndpointIDSet[endpointID] = struct{}{}
}
} else {
edgeGroups[n] = edgeGroup
n++
@ -388,7 +372,7 @@ func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGr
n = 0
for _, endpoint := range endpoints {
if !excludeEndpointIDSet.Contains(endpoint.ID) {
if _, ok := excludeEndpointIDSet[endpoint.ID]; !ok {
endpoints[n] = endpoint
n++
}
@ -613,10 +597,15 @@ func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids roar.Roar[portainer.EndpointID]) []portainer.Endpoint {
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
idsSet := make(map[portainer.EndpointID]bool, len(ids))
for _, id := range ids {
idsSet[id] = true
}
n := 0
for _, endpoint := range endpoints {
if ids.Contains(endpoint.ID) {
if idsSet[endpoint.ID] {
endpoints[n] = endpoint
n++
}

View file

@ -6,13 +6,10 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/roar"
"github.com/portainer/portainer/api/slicesx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type filterTest struct {
@ -177,7 +174,7 @@ func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
EndpointIDs: roar.FromSlice(endpointIDs),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1, 2, 3},
PartialMatch: true,
@ -224,11 +221,11 @@ func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
edgeGroups := []portainer.EdgeGroup{}
for i := range 1000 {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
EndpointIDs: roar.FromSlice(endpointIDs),
Dynamic: true,
TagIDs: []portainer.TagID{1},
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1},
})
}
@ -266,7 +263,6 @@ func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portai
[]portainer.EndpointGroup{},
[]portainer.EdgeGroup{},
&portainer.Settings{},
&security.RestrictedRequestContext{IsAdmin: true},
)
is.NoError(err)
@ -302,127 +298,3 @@ func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) *Handler {
return handler
}
func TestFilterEndpointsByEdgeStack(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, false)
endpoints := []portainer.Endpoint{
{ID: 1, Name: "Endpoint 1"},
{ID: 2, Name: "Endpoint 2"},
{ID: 3, Name: "Endpoint 3"},
{ID: 4, Name: "Endpoint 4"},
}
edgeStackId := portainer.EdgeStackID(1)
err := store.EdgeStack().Create(edgeStackId, &portainer.EdgeStack{
ID: edgeStackId,
Name: "Test Edge Stack",
EdgeGroups: []portainer.EdgeGroupID{1, 2},
})
require.NoError(t, err)
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Edge Group 1",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
})
require.NoError(t, err)
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 2,
Name: "Edge Group 2",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
})
require.NoError(t, err)
es, err := filterEndpointsByEdgeStack(endpoints, edgeStackId, nil, store)
require.NoError(t, err)
require.Len(t, es, 3)
require.Contains(t, es, endpoints[0]) // Endpoint 1
require.Contains(t, es, endpoints[1]) // Endpoint 2
require.Contains(t, es, endpoints[2]) // Endpoint 3
require.NotContains(t, es, endpoints[3]) // Endpoint 4
}
func TestFilterEndpointsByEdgeGroup(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, false)
endpoints := []portainer.Endpoint{
{ID: 1, Name: "Endpoint 1"},
{ID: 2, Name: "Endpoint 2"},
{ID: 3, Name: "Endpoint 3"},
{ID: 4, Name: "Endpoint 4"},
}
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Edge Group 1",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
})
require.NoError(t, err)
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 2,
Name: "Edge Group 2",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
})
require.NoError(t, err)
edgeGroups, err := store.EdgeGroup().ReadAll()
require.NoError(t, err)
es, egs := filterEndpointsByEdgeGroupIDs(endpoints, edgeGroups, []portainer.EdgeGroupID{1, 2})
require.NoError(t, err)
require.Len(t, es, 3)
require.Contains(t, es, endpoints[0]) // Endpoint 1
require.Contains(t, es, endpoints[1]) // Endpoint 2
require.Contains(t, es, endpoints[2]) // Endpoint 3
require.NotContains(t, es, endpoints[3]) // Endpoint 4
require.Len(t, egs, 2)
require.Equal(t, egs[0].ID, portainer.EdgeGroupID(1))
require.Equal(t, egs[1].ID, portainer.EdgeGroupID(2))
}
func TestFilterEndpointsByExcludeEdgeGroupIDs(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, false)
endpoints := []portainer.Endpoint{
{ID: 1, Name: "Endpoint 1"},
{ID: 2, Name: "Endpoint 2"},
{ID: 3, Name: "Endpoint 3"},
{ID: 4, Name: "Endpoint 4"},
}
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 1,
Name: "Edge Group 1",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
})
require.NoError(t, err)
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
ID: 2,
Name: "Edge Group 2",
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
})
require.NoError(t, err)
edgeGroups, err := store.EdgeGroup().ReadAll()
require.NoError(t, err)
es, egs := filterEndpointsByExcludeEdgeGroupIDs(endpoints, edgeGroups, []portainer.EdgeGroupID{1})
require.NoError(t, err)
require.Len(t, es, 3)
require.Equal(t, es, []portainer.Endpoint{
{ID: 2, Name: "Endpoint 2"},
{ID: 3, Name: "Endpoint 3"},
{ID: 4, Name: "Endpoint 4"},
})
require.Len(t, egs, 1)
require.Equal(t, egs[0].ID, portainer.EdgeGroupID(2))
}

View file

@ -17,7 +17,17 @@ func (handler *Handler) updateEdgeRelations(tx dataservices.DataStoreTx, endpoin
relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
return errors.WithMessage(err, "Unable to retrieve environment relation inside the database")
if !tx.IsErrObjectNotFound(err) {
return errors.WithMessage(err, "Unable to retrieve environment relation inside the database")
}
relation = &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
if err := tx.EndpointRelation().Create(relation); err != nil {
return errors.WithMessage(err, "Unable to create environment relation inside the database")
}
}
endpointGroup, err := tx.EndpointGroup().Read(endpoint.GroupID)

View file

@ -1,11 +1,12 @@
package endpoints
import (
"slices"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/set"
"github.com/pkg/errors"
)
func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []portainer.EdgeGroupID, environmentID portainer.EndpointID) (bool, error) {
@ -18,8 +19,10 @@ func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []po
environmentEdgeGroupsSet := set.Set[portainer.EdgeGroupID]{}
for _, edgeGroup := range edgeGroups {
if edgeGroup.EndpointIDs.Contains(environmentID) {
environmentEdgeGroupsSet[edgeGroup.ID] = true
for _, eID := range edgeGroup.Endpoints {
if eID == environmentID {
environmentEdgeGroupsSet[edgeGroup.ID] = true
}
}
}
@ -49,16 +52,20 @@ func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []po
}
removeEdgeGroups := environmentEdgeGroupsSet.Difference(newEdgeGroupsSet)
if err := updateSet(removeEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
edgeGroup.EndpointIDs.Remove(environmentID)
}); err != nil {
err = updateSet(removeEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
edgeGroup.Endpoints = slices.DeleteFunc(edgeGroup.Endpoints, func(eID portainer.EndpointID) bool {
return eID == environmentID
})
})
if err != nil {
return false, err
}
addToEdgeGroups := newEdgeGroupsSet.Difference(environmentEdgeGroupsSet)
if err := updateSet(addToEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
edgeGroup.EndpointIDs.Add(environmentID)
}); err != nil {
err = updateSet(addToEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
edgeGroup.Endpoints = append(edgeGroup.Endpoints, environmentID)
})
if err != nil {
return false, err
}

View file

@ -6,7 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
@ -15,9 +14,10 @@ func Test_updateEdgeGroups(t *testing.T) {
groups := make([]portainer.EdgeGroup, len(names))
for index, name := range names {
group := &portainer.EdgeGroup{
Name: name,
Dynamic: false,
TagIDs: make([]portainer.TagID, 0),
Name: name,
Dynamic: false,
TagIDs: make([]portainer.TagID, 0),
Endpoints: make([]portainer.EndpointID, 0),
}
if err := store.EdgeGroup().Create(group); err != nil {
@ -35,8 +35,13 @@ func Test_updateEdgeGroups(t *testing.T) {
group, err := store.EdgeGroup().Read(groupID)
is.NoError(err)
is.True(group.EndpointIDs.Contains(endpointID),
"expected endpoint to be in group")
for _, endpoint := range group.Endpoints {
if endpoint == endpointID {
return
}
}
is.Fail("expected endpoint to be in group")
}
}
@ -76,7 +81,7 @@ func Test_updateEdgeGroups(t *testing.T) {
endpointGroups := groupsByName(groups, testCase.endpointGroupNames)
for _, group := range endpointGroups {
group.EndpointIDs.Add(testCase.endpoint.ID)
group.Endpoints = append(group.Endpoints, testCase.endpoint.ID)
err = store.EdgeGroup().Update(group.ID, &group)
is.NoError(err)

View file

@ -10,6 +10,7 @@ import (
)
func Test_updateTags(t *testing.T) {
createTags := func(store *datastore.Store, tagNames []string) ([]portainer.Tag, error) {
tags := make([]portainer.Tag, len(tagNames))
for index, tagName := range tagNames {

View file

@ -17,12 +17,12 @@ type Handler struct {
}
// NewHandler creates a handler to serve static files.
func NewHandler(assetPublicPath string, csp bool, wasInstanceDisabled func() bool) *Handler {
func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler {
h := &Handler{
Handler: security.MWSecureHeaders(
gzhttp.GzipHandler(http.FileServer(http.Dir(assetPublicPath))),
featureflags.IsEnabled("hsts"),
csp,
featureflags.IsEnabled("csp"),
),
wasInstanceDisabled: wasInstanceDisabled,
}
@ -36,7 +36,6 @@ func isHTML(acceptContent []string) bool {
return true
}
}
return false
}
@ -44,13 +43,11 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler.wasInstanceDisabled() {
if r.RequestURI == "/" || r.RequestURI == "/index.html" {
http.Redirect(w, r, "/timeout.html", http.StatusTemporaryRedirect)
return
}
} else {
if strings.HasPrefix(r.RequestURI, "/timeout.html") {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
}

View file

@ -17,11 +17,10 @@ type fileResponse struct {
}
type repositoryFilePreviewPayload struct {
Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"`
Reference string `json:"reference" example:"refs/heads/master"`
Username string `json:"username" example:"myGitUsername"`
Password string `json:"password" example:"myGitPassword"`
AuthorizationType gittypes.GitCredentialAuthType `json:"authorizationType"`
Repository string `json:"repository" example:"https://github.com/openfaas/faas" validate:"required"`
Reference string `json:"reference" example:"refs/heads/master"`
Username string `json:"username" example:"myGitUsername"`
Password string `json:"password" example:"myGitPassword"`
// Path to file whose content will be read
TargetFile string `json:"targetFile" example:"docker-compose.yml"`
// TLSSkipVerify skips SSL verification when cloning the Git repository
@ -69,15 +68,7 @@ func (handler *Handler) gitOperationRepoFilePreview(w http.ResponseWriter, r *ht
return httperror.InternalServerError("Unable to create temporary folder", err)
}
err = handler.gitService.CloneRepository(
projectPath,
payload.Repository,
payload.Reference,
payload.Username,
payload.Password,
payload.AuthorizationType,
payload.TLSSkipVerify,
)
err = handler.gitService.CloneRepository(projectPath, payload.Repository, payload.Reference, payload.Username, payload.Password, payload.TLSSkipVerify)
if err != nil {
if errors.Is(err, gittypes.ErrAuthenticationFailure) {
return httperror.BadRequest("Invalid git credential", err)

View file

@ -81,7 +81,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.32.0
// @version 2.30.0
// @description.markdown api-description.md
// @termsOfService

View file

@ -46,24 +46,18 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param payload body installChartPayload true "Chart details"
// @param dryRun query bool false "Dry run"
// @success 201 {object} release.Release "Created"
// @failure 401 "Unauthorized"
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
// @failure 500 "Server error"
// @router /endpoints/{id}/kubernetes/helm [post]
func (handler *Handler) helmInstall(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
dryRun, err := request.RetrieveBooleanQueryParameter(r, "dryRun", true)
if err != nil {
return httperror.BadRequest("Invalid dryRun query parameter", err)
}
var payload installChartPayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
return httperror.BadRequest("Invalid Helm install payload", err)
}
release, err := handler.installChart(r, payload, dryRun)
release, err := handler.installChart(r, payload)
if err != nil {
return httperror.InternalServerError("Unable to install a chart", err)
}
@ -100,7 +94,7 @@ func (p *installChartPayload) Validate(_ *http.Request) error {
return nil
}
func (handler *Handler) installChart(r *http.Request, p installChartPayload, dryRun bool) (*release.Release, error) {
func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*release.Release, error) {
clusterAccess, httperr := handler.getHelmClusterAccess(r)
if httperr != nil {
return nil, httperr.Err
@ -113,7 +107,6 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload, dry
Namespace: p.Namespace,
Repo: p.Repo,
Atomic: p.Atomic,
DryRun: dryRun,
KubernetesClusterAccess: clusterAccess,
}
@ -141,14 +134,13 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload, dry
return nil, err
}
if !installOpts.DryRun {
manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest)
if err != nil {
return nil, err
}
if err := handler.updateHelmAppManifest(r, manifest, installOpts.Namespace); err != nil {
return nil, err
}
manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest)
if err != nil {
return nil, err
}
if err := handler.updateHelmAppManifest(r, manifest, installOpts.Namespace); err != nil {
return nil, err
}
return release, nil

View file

@ -7,7 +7,6 @@ import (
"github.com/portainer/portainer/pkg/libhelm/options"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/pkg/errors"
)
@ -18,8 +17,6 @@ import (
// @description **Access policy**: authenticated
// @tags helm
// @param repo query string true "Helm repository URL"
// @param chart query string false "Helm chart name"
// @param useCache query string false "If true will use cache to search"
// @security ApiKeyAuth
// @security jwt
// @produce json
@ -35,19 +32,13 @@ func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *
return httperror.BadRequest("Bad request", errors.New("missing `repo` query parameter"))
}
chart, _ := request.RetrieveQueryParameter(r, "chart", false)
// If true will useCache to search, will always add to cache after
useCache, _ := request.RetrieveBooleanQueryParameter(r, "useCache", false)
_, err := url.ParseRequestURI(repo)
if err != nil {
return httperror.BadRequest("Bad request", errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo)))
}
searchOpts := options.SearchRepoOptions{
Repo: repo,
Chart: chart,
UseCache: useCache,
Repo: repo,
}
result, err := handler.helmPackageManager.SearchRepo(searchOpts)

View file

@ -20,7 +20,6 @@ import (
// @tags helm
// @param repo query string true "Helm repository URL"
// @param chart query string true "Chart name"
// @param version query string true "Chart version"
// @param command path string true "chart/values/readme"
// @security ApiKeyAuth
// @security jwt
@ -46,11 +45,6 @@ func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httper
return httperror.BadRequest("Bad request", errors.New("missing `chart` query parameter"))
}
version, err := request.RetrieveQueryParameter(r, "version", true)
if err != nil {
return httperror.BadRequest("Bad request", errors.Wrap(err, fmt.Sprintf("provided version %q is not valid", version)))
}
cmd, err := request.RetrieveRouteVariableValue(r, "command")
if err != nil {
cmd = "all"
@ -61,7 +55,6 @@ func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httper
OutputFormat: options.ShowOutputFormat(cmd),
Chart: chart,
Repo: repo,
Version: version,
}
result, err := handler.helmPackageManager.Show(showOptions)
if err != nil {

View file

@ -2,10 +2,8 @@ package kubernetes
import (
"net/http"
"strconv"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/rs/zerolog/log"
@ -27,19 +25,13 @@ func (handler *Handler) prepareKubeClient(r *http.Request) (*cli.KubeClient, *ht
return nil, httperror.NotFound("Unable to find the Kubernetes endpoint associated to the request.", err)
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to retrieve token data associated to the request.")
return nil, httperror.InternalServerError("Unable to retrieve token data associated to the request.", err)
}
pcli, err := handler.KubernetesClientFactory.GetPrivilegedUserKubeClient(endpoint, strconv.Itoa(int(tokenData.ID)))
pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
log.Error().Err(err).Str("context", "prepareKubeClient").Msg("Unable to get a privileged Kubernetes client for the user.")
return nil, httperror.InternalServerError("Unable to get a privileged Kubernetes client for the user.", err)
}
pcli.SetIsKubeAdmin(cli.GetIsKubeAdmin())
pcli.SetClientNonAdminNamespaces(cli.GetClientNonAdminNamespaces())
pcli.IsKubeAdmin = cli.IsKubeAdmin
pcli.NonAdminNamespaces = cli.NonAdminNamespaces
return pcli, nil
}

View file

@ -32,7 +32,7 @@ func (handler *Handler) getAllKubernetesClusterRoleBindings(w http.ResponseWrite
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", httpErr)
}
if !cli.GetIsKubeAdmin() {
if !cli.IsKubeAdmin {
log.Error().Str("context", "getAllKubernetesClusterRoleBindings").Msg("user is not authorized to fetch cluster role bindings from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster role bindings from the Kubernetes cluster.", nil)
}

View file

@ -32,7 +32,7 @@ func (handler *Handler) getAllKubernetesClusterRoles(w http.ResponseWriter, r *h
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", httpErr)
}
if !cli.GetIsKubeAdmin() {
if !cli.IsKubeAdmin {
log.Error().Str("context", "getAllKubernetesClusterRoles").Msg("user is not authorized to fetch cluster roles from the Kubernetes cluster.")
return httperror.Forbidden("User is not authorized to fetch cluster roles from the Kubernetes cluster.", nil)
}

View file

@ -1,102 +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/rs/zerolog/log"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
// @id getKubernetesEventsForNamespace
// @summary Gets kubernetes events for namespace
// @description Get events by optional query param resourceId for a given namespace.
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param namespace path string true "The namespace name the events are associated to"
// @param resourceId query string false "The resource id of the involved kubernetes object" example:"e5b021b6-4bce-4c06-bd3b-6cca906797aa"
// @success 200 {object} []kubernetes.K8sEvent "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 500 "Server error occurred while attempting to retrieve the events within the specified namespace."
// @router /kubernetes/{id}/namespaces/{namespace}/events [get]
func (handler *Handler) getKubernetesEventsForNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
namespace, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesEvents").Str("namespace", namespace).Msg("Unable to retrieve namespace identifier route variable")
return httperror.BadRequest("Unable to retrieve namespace identifier route variable", err)
}
resourceId, err := request.RetrieveQueryParameter(r, "resourceId", true)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve resourceId query parameter")
return httperror.BadRequest("Unable to retrieve resourceId query parameter", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getKubernetesEvents").Str("resourceId", resourceId).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
}
events, err := cli.GetEvents(namespace, resourceId)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve events")
return httperror.InternalServerError("Unable to retrieve events", err)
}
return response.JSON(w, events)
}
// @id getAllKubernetesEvents
// @summary Gets kubernetes events
// @description Get events by query param resourceId
// @description **Access policy**: Authenticated user.
// @tags kubernetes
// @security ApiKeyAuth || jwt
// @produce json
// @param id path int true "Environment identifier"
// @param resourceId query string false "The resource id of the involved kubernetes object" example:"e5b021b6-4bce-4c06-bd3b-6cca906797aa"
// @success 200 {object} []kubernetes.K8sEvent "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 500 "Server error occurred while attempting to retrieve the events."
// @router /kubernetes/{id}/events [get]
func (handler *Handler) getAllKubernetesEvents(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
resourceId, err := request.RetrieveQueryParameter(r, "resourceId", true)
if err != nil {
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve resourceId query parameter")
return httperror.BadRequest("Unable to retrieve resourceId query parameter", err)
}
cli, httpErr := handler.getProxyKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "getKubernetesEvents").Str("resourceId", resourceId).Msg("Unable to get a Kubernetes client for the user")
return httperror.InternalServerError("Unable to get a Kubernetes client for the user", httpErr)
}
events, err := cli.GetEvents("", resourceId)
if err != nil {
if k8serrors.IsUnauthorized(err) || k8serrors.IsForbidden(err) {
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unauthorized access to the Kubernetes API")
return httperror.Forbidden("Unauthorized access to the Kubernetes API", err)
}
log.Error().Err(err).Str("context", "getKubernetesEvents").Msg("Unable to retrieve events")
return httperror.InternalServerError("Unable to retrieve events", err)
}
return response.JSON(w, events)
}

View file

@ -1,60 +0,0 @@
package kubernetes
import (
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
kubeClient "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/stretchr/testify/assert"
)
// Currently this test just tests the HTTP Handler is setup correctly, in the future we should move the ClientFactory to a mock in order
// test the logic in event.go
func TestGetKubernetesEvents(t *testing.T) {
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
err := store.Endpoint().Create(&portainer.Endpoint{
ID: 1,
Type: portainer.AgentOnKubernetesEnvironment,
},
)
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")
tk, _, _ := jwtService.GenerateToken(&portainer.TokenData{ID: 1, Username: "admin", Role: portainer.AdministratorRole})
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
cli := testhelpers.NewKubernetesClient()
factory, _ := kubeClient.NewClientFactory(nil, nil, store, "", "", "")
authorizationService := authorization.NewService(store)
handler := NewHandler(testhelpers.NewTestRequestBouncer(), authorizationService, store, jwtService, kubeClusterAccessService,
factory, cli)
is.NotNil(handler, "Handler should not fail")
req := httptest.NewRequest(http.MethodGet, "/kubernetes/1/events?resourceId=8", nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
testhelpers.AddTestSecurityCookie(req, tk)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
}

View file

@ -58,7 +58,6 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
endpointRouter.Handle("/configmaps/count", httperror.LoggerHandler(h.getAllKubernetesConfigMapsCount)).Methods(http.MethodGet)
endpointRouter.Handle("/cron_jobs", httperror.LoggerHandler(h.getAllKubernetesCronJobs)).Methods(http.MethodGet)
endpointRouter.Handle("/cron_jobs/delete", httperror.LoggerHandler(h.deleteKubernetesCronJobs)).Methods(http.MethodPost)
endpointRouter.Handle("/events", httperror.LoggerHandler(h.getAllKubernetesEvents)).Methods(http.MethodGet)
endpointRouter.Handle("/jobs", httperror.LoggerHandler(h.getAllKubernetesJobs)).Methods(http.MethodGet)
endpointRouter.Handle("/jobs/delete", httperror.LoggerHandler(h.deleteKubernetesJobs)).Methods(http.MethodPost)
endpointRouter.Handle("/cluster_roles", httperror.LoggerHandler(h.getAllKubernetesClusterRoles)).Methods(http.MethodGet)
@ -111,7 +110,6 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
// to keep it simple, we've decided to leave it like this.
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
namespaceRouter.Handle("/events", httperror.LoggerHandler(h.getKubernetesEventsForNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
@ -135,7 +133,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
// getProxyKubeClient gets a kubeclient for the user. It's generally what you want as it retrieves the kubeclient
// from the Authorization token of the currently logged in user. The kubeclient that is not from the proxy is actually using
// admin permissions. If you're unsure which one to use, use this.
func (h *Handler) getProxyKubeClient(r *http.Request) (portainer.KubeClient, *httperror.HandlerError) {
func (h *Handler) getProxyKubeClient(r *http.Request) (*cli.KubeClient, *httperror.HandlerError) {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return nil, httperror.BadRequest(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, the environment identifier route variable is invalid for /api/kubernetes/%d. Error: ", endpointID), err)
@ -146,7 +144,7 @@ func (h *Handler) getProxyKubeClient(r *http.Request) (portainer.KubeClient, *ht
return nil, httperror.Forbidden(fmt.Sprintf("an error occurred during the getProxyKubeClient operation, permission denied to access the environment /api/kubernetes/%d. Error: ", endpointID), err)
}
cli, ok := h.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), strconv.Itoa(int(tokenData.ID)))
cli, ok := h.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), tokenData.Token)
if !ok {
return nil, httperror.InternalServerError("an error occurred during the getProxyKubeClient operation,failed to get proxy KubeClient", nil)
}
@ -179,7 +177,7 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
}
// Check if we have a kubeclient against this auth token already, otherwise generate a new one
_, ok := handler.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), strconv.Itoa(int(tokenData.ID)))
_, ok := handler.KubernetesClientFactory.GetProxyKubeClient(strconv.Itoa(endpointID), tokenData.Token)
if ok {
next.ServeHTTP(w, r)
return
@ -255,7 +253,7 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
return
}
serverURL.Scheme = "https"
serverURL.Host = "localhost" + handler.KubernetesClientFactory.GetAddrHTTPS()
serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS
config.Clusters[0].Cluster.Server = serverURL.String()
yaml, err := cli.GenerateYAML(config)
@ -269,7 +267,7 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler {
return
}
handler.KubernetesClientFactory.SetProxyKubeClient(strconv.Itoa(int(endpoint.ID)), strconv.Itoa(int(tokenData.ID)), kubeCli)
handler.KubernetesClientFactory.SetProxyKubeClient(strconv.Itoa(int(endpoint.ID)), tokenData.Token, kubeCli)
next.ServeHTTP(w, r)
})
}

View file

@ -22,7 +22,6 @@ import (
// @produce json
// @param id path int true "Environment identifier"
// @param withResourceQuota query boolean true "When set to true, include the resource quota information as part of the Namespace information. Default is false"
// @param withUnhealthyEvents query boolean true "When set to true, include the unhealthy events information as part of the Namespace information. Default is false"
// @success 200 {array} portainer.K8sNamespaceInfo "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."
@ -37,12 +36,6 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
return httperror.BadRequest("an error occurred during the GetKubernetesNamespaces operation, invalid query parameter withResourceQuota. Error: ", err)
}
withUnhealthyEvents, err := request.RetrieveBooleanQueryParameter(r, "withUnhealthyEvents", true)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Invalid query parameter withUnhealthyEvents")
return httperror.BadRequest("an error occurred during the GetKubernetesNamespaces operation, invalid query parameter withUnhealthyEvents. Error: ", err)
}
cli, httpErr := handler.prepareKubeClient(r)
if httpErr != nil {
log.Error().Err(httpErr).Str("context", "GetKubernetesNamespaces").Msg("Unable to get a Kubernetes client for the user")
@ -55,14 +48,6 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to retrieve namespaces from the Kubernetes cluster. Error: ", err)
}
if withUnhealthyEvents {
namespaces, err = cli.CombineNamespacesWithUnhealthyEvents(namespaces)
if err != nil {
log.Error().Err(err).Str("context", "GetKubernetesNamespaces").Msg("Unable to combine namespaces with unhealthy events")
return httperror.InternalServerError("an error occurred during the GetKubernetesNamespaces operation, unable to combine namespaces with unhealthy events. Error: ", err)
}
}
if withResourceQuota {
return cli.CombineNamespacesWithResourceQuotas(namespaces, w)
}

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: ""})

View file

@ -5,10 +5,10 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/registryutils/access"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
@ -17,7 +17,6 @@ import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
func hideFields(registry *portainer.Registry, hideAccesses bool) {
@ -57,20 +56,17 @@ func newHandler(bouncer security.BouncerService) *Handler {
func (handler *Handler) initRouter(bouncer accessGuard) {
adminRouter := handler.NewRoute().Subrouter()
adminRouter.Use(bouncer.AdminAccess)
authenticatedRouter := handler.NewRoute().Subrouter()
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
adminRouter.Handle("/registries", httperror.LoggerHandler(handler.registryList)).Methods(http.MethodGet)
adminRouter.Handle("/registries", httperror.LoggerHandler(handler.registryCreate)).Methods(http.MethodPost)
adminRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryUpdate)).Methods(http.MethodPut)
adminRouter.Handle("/registries/{id}/configure", httperror.LoggerHandler(handler.registryConfigure)).Methods(http.MethodPost)
adminRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryDelete)).Methods(http.MethodDelete)
// Use registry-specific access bouncer for inspect and repositories endpoints
registryAccessRouter := handler.NewRoute().Subrouter()
registryAccessRouter.Use(bouncer.AuthenticatedAccess, handler.RegistryAccess)
registryAccessRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryInspect)).Methods(http.MethodGet)
// Keep the gitlab proxy on the regular authenticated router as it doesn't require specific registry access
authenticatedRouter := handler.NewRoute().Subrouter()
authenticatedRouter.Use(bouncer.AuthenticatedAccess)
authenticatedRouter.Handle("/registries/{id}", httperror.LoggerHandler(handler.registryInspect)).Methods(http.MethodGet)
authenticatedRouter.PathPrefix("/registries/proxies/gitlab").Handler(httperror.LoggerHandler(handler.proxyRequestsToGitlabAPIWithoutRegistry))
}
@ -92,7 +88,9 @@ func (handler *Handler) registriesHaveSameURLAndCredentials(r1, r2 *portainer.Re
}
// this function validates that
//
// 1. user has the appropriate authorizations to perform the request
//
// 2. user has a direct or indirect access to the registry
func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portainer.Registry) (hasAccess bool, isAdmin bool, err error) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
@ -100,6 +98,11 @@ func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portain
return false, false, err
}
user, err := handler.DataStore.User().Read(securityContext.UserID)
if err != nil {
return false, false, err
}
// Portainer admins always have access to everything
if securityContext.IsAdmin {
return true, true, nil
@ -125,68 +128,47 @@ func (handler *Handler) userHasRegistryAccess(r *http.Request, registry *portain
return false, false, err
}
// Use the enhanced registry access utility function that includes namespace validation
_, err = access.GetAccessibleRegistry(
handler.DataStore,
handler.K8sClientFactory,
securityContext.UserID,
endpointId,
registry.ID,
)
memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return false, false, nil // No access
return false, false, nil
}
return true, false, nil
}
// RegistryAccess defines a security check for registry-specific API endpoints.
// Authentication is required to access these endpoints.
// The user must have direct or indirect access to the specific registry being requested.
// This bouncer validates registry access using the userHasRegistryAccess logic.
func (handler *Handler) RegistryAccess(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// First ensure the user is authenticated
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteError(w, http.StatusUnauthorized, "Authentication required", httperrors.ErrUnauthorized)
return
}
// Extract registry ID from the route
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
httperror.WriteError(w, http.StatusBadRequest, "Invalid registry identifier route variable", err)
return
}
// Get the registry from the database
registry, err := handler.DataStore.Registry().Read(portainer.RegistryID(registryID))
if handler.DataStore.IsErrObjectNotFound(err) {
httperror.WriteError(w, http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err)
return
} else if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err)
return
}
// Check if user has access to this registry
hasAccess, _, err := handler.userHasRegistryAccess(r, registry)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve info from request context", err)
return
}
if !hasAccess {
log.Debug().
Int("registry_id", registryID).
Str("registry_name", registry.Name).
Int("user_id", int(tokenData.ID)).
Str("context", "RegistryAccessBouncer").
Msg("User access denied to registry")
httperror.WriteError(w, http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied)
return
}
next.ServeHTTP(w, r)
})
// validate access for kubernetes namespaces (leverage registry.RegistryAccesses[endpointId].Namespaces)
if endpointutils.IsKubernetesEndpoint(endpoint) {
kcl, err := handler.K8sClientFactory.GetPrivilegedKubeClient(endpoint)
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve kubernetes client to validate registry access")
}
accessPolicies, err := kcl.GetNamespaceAccessPolicies()
if err != nil {
return false, false, errors.Wrap(err, "unable to retrieve environment's namespaces policies to validate registry access")
}
authorizedNamespaces := registry.RegistryAccesses[endpointId].Namespaces
for _, namespace := range authorizedNamespaces {
// when the default namespace is authorized to use a registry, all users have the ability to use it
// unless the default namespace is restricted: in this case continue to search for other potential accesses authorizations
if namespace == kubernetes.DefaultNamespace && !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace {
return true, false, nil
}
namespacePolicy := accessPolicies[namespace]
if security.AuthorizedAccess(user.ID, memberships, namespacePolicy.UserAccessPolicies, namespacePolicy.TeamAccessPolicies) {
return true, false, nil
}
}
return false, false, nil
}
// validate access for docker environments
// leverage registry.RegistryAccesses[endpointId].UserAccessPolicies (direct access)
// and registry.RegistryAccesses[endpointId].TeamAccessPolicies (indirect access via his teams)
if security.AuthorizedRegistryAccess(registry, user, memberships, endpoint.ID) {
return true, false, nil
}
// when user has no access via their role, direct grant or indirect grant
// then they don't have access to the registry
return false, false, nil
}

View file

@ -1,89 +0,0 @@
package registries
import (
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func Test_RegistryAccess_RequiresAuthentication(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
registry := &portainer.Registry{
ID: 1,
Name: "test-registry",
URL: "https://registry.test.com",
}
err := store.Registry().Create(registry)
assert.NoError(t, err)
handler := &Handler{
DataStore: store,
}
req := httptest.NewRequest(http.MethodGet, "/registries/1", nil)
req = mux.SetURLVars(req, map[string]string{"id": "1"})
rr := httptest.NewRecorder()
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
bouncer := handler.RegistryAccess(testHandler)
bouncer.ServeHTTP(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
}
func Test_RegistryAccess_InvalidRegistryID(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
user := &portainer.User{ID: 1, Username: "test", Role: portainer.StandardUserRole}
err := store.User().Create(user)
assert.NoError(t, err)
handler := &Handler{
DataStore: store,
}
req := httptest.NewRequest(http.MethodGet, "/registries/invalid", nil)
req = mux.SetURLVars(req, map[string]string{"id": "invalid"})
tokenData := &portainer.TokenData{ID: 1, Role: portainer.StandardUserRole}
req = req.WithContext(security.StoreTokenData(req, tokenData))
rr := httptest.NewRecorder()
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
bouncer := handler.RegistryAccess(testHandler)
bouncer.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
func Test_RegistryAccess_RegistryNotFound(t *testing.T) {
_, store := datastore.MustNewTestStore(t, true, true)
user := &portainer.User{ID: 1, Username: "test", Role: portainer.StandardUserRole}
err := store.User().Create(user)
assert.NoError(t, err)
handler := &Handler{
DataStore: store,
requestBouncer: testhelpers.NewTestRequestBouncer(),
}
req := httptest.NewRequest(http.MethodGet, "/registries/999", nil)
req = mux.SetURLVars(req, map[string]string{"id": "999"})
tokenData := &portainer.TokenData{ID: 1, Role: portainer.StandardUserRole}
req = req.WithContext(security.StoreTokenData(req, tokenData))
rr := httptest.NewRecorder()
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
bouncer := handler.RegistryAccess(testHandler)
bouncer.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
}

View file

@ -4,12 +4,10 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
httperrors "github.com/portainer/portainer/api/http/errors"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
"github.com/rs/zerolog/log"
)
// @id RegistryInspect
@ -33,11 +31,6 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return httperror.BadRequest("Invalid registry identifier route variable", err)
}
log.Debug().
Int("registry_id", registryID).
Str("context", "RegistryInspectHandler").
Msg("Starting registry inspection")
registry, err := handler.DataStore.Registry().Read(portainer.RegistryID(registryID))
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find a registry with the specified identifier inside the database", err)
@ -45,12 +38,14 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return httperror.InternalServerError("Unable to find a registry with the specified identifier inside the database", err)
}
// Check if user is admin to determine if we should hide sensitive fields
securityContext, err := security.RetrieveRestrictedRequestContext(r)
hasAccess, isAdmin, err := handler.userHasRegistryAccess(r, registry)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
if !hasAccess {
return httperror.Forbidden("Access denied to resource", httperrors.ErrResourceAccessDenied)
}
hideFields(registry, !securityContext.IsAdmin)
hideFields(registry, !isAdmin)
return response.JSON(w, registry)
}

View file

@ -19,15 +19,14 @@ import (
)
type stackGitUpdatePayload struct {
AutoUpdate *portainer.AutoUpdateSettings
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
RepositoryAuthorizationType gittypes.GitCredentialAuthType
TLSSkipVerify bool
AutoUpdate *portainer.AutoUpdateSettings
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
TLSSkipVerify bool
}
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
@ -152,19 +151,11 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
AuthorizationType: payload.RepositoryAuthorizationType,
Username: payload.RepositoryUsername,
Password: password,
}
if _, err := handler.GitService.LatestCommitID(
stack.GitConfig.URL,
stack.GitConfig.ReferenceName,
stack.GitConfig.Authentication.Username,
stack.GitConfig.Authentication.Password,
stack.GitConfig.Authentication.AuthorizationType,
stack.GitConfig.TLSSkipVerify,
); err != nil {
if _, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify); err != nil {
return httperror.InternalServerError("Unable to fetch git repository", err)
}
} else {

View file

@ -6,7 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/git"
gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
k "github.com/portainer/portainer/api/kubernetes"
@ -20,13 +19,12 @@ import (
)
type stackGitRedployPayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
RepositoryAuthorizationType gittypes.GitCredentialAuthType
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
Env []portainer.Pair
Prune bool
// Force a pulling to current image with the original tag though the image is already the latest
PullImage bool `example:"false"`
@ -137,16 +135,13 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
repositoryUsername := ""
repositoryPassword := ""
repositoryAuthType := gittypes.GitCredentialAuthType_Basic
if payload.RepositoryAuthentication {
repositoryPassword = payload.RepositoryPassword
repositoryAuthType = payload.RepositoryAuthorizationType
// When the existing stack is using the custom username/password and the password is not updated,
// the stack should keep using the saved username/password
if repositoryPassword == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
repositoryPassword = stack.GitConfig.Authentication.Password
repositoryAuthType = stack.GitConfig.Authentication.AuthorizationType
}
repositoryUsername = payload.RepositoryUsername
}
@ -157,7 +152,6 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
ReferenceName: stack.GitConfig.ReferenceName,
Username: repositoryUsername,
Password: repositoryPassword,
AuthType: repositoryAuthType,
TLSSkipVerify: stack.GitConfig.TLSSkipVerify,
}
@ -172,7 +166,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
return err
}
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, repositoryAuthType, stack.GitConfig.TLSSkipVerify)
newHash, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, repositoryUsername, repositoryPassword, stack.GitConfig.TLSSkipVerify)
if err != nil {
return httperror.InternalServerError("Unable get latest commit id", errors.WithMessagef(err, "failed to fetch latest commit id of the stack %v", stack.ID))
}

View file

@ -27,13 +27,12 @@ type kubernetesFileStackUpdatePayload struct {
}
type kubernetesGitStackUpdatePayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
RepositoryAuthorizationType gittypes.GitCredentialAuthType
AutoUpdate *portainer.AutoUpdateSettings
TLSSkipVerify bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
AutoUpdate *portainer.AutoUpdateSettings
TLSSkipVerify bool
}
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
@ -77,19 +76,11 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
AuthorizationType: payload.RepositoryAuthorizationType,
Username: payload.RepositoryUsername,
Password: password,
}
if _, err := handler.GitService.LatestCommitID(
stack.GitConfig.URL,
stack.GitConfig.ReferenceName,
stack.GitConfig.Authentication.Username,
stack.GitConfig.Authentication.Password,
stack.GitConfig.Authentication.AuthorizationType,
stack.GitConfig.TLSSkipVerify,
); err != nil {
if _, err := handler.GitService.LatestCommitID(stack.GitConfig.URL, stack.GitConfig.ReferenceName, stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password, stack.GitConfig.TLSSkipVerify); err != nil {
return httperror.InternalServerError("Unable to fetch git repository", err)
}
}

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 ""
}

View file

@ -8,7 +8,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/edge"
"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/libhttp/response"
@ -59,9 +58,6 @@ func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error {
for endpointID := range tag.Endpoints {
endpoint, err := tx.Endpoint().Endpoint(endpointID)
if tx.IsErrObjectNotFound(err) {
continue
}
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
}
@ -107,10 +103,15 @@ func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error {
return httperror.InternalServerError("Unable to retrieve edge stacks from the database", err)
}
edgeJobs, err := tx.EdgeJob().ReadAll()
if err != nil {
return httperror.InternalServerError("Unable to retrieve edge job configurations from the database", err)
for _, endpoint := range endpoints {
if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) {
err = updateEndpointRelations(tx, endpoint, edgeGroups, edgeStacks)
if err != nil {
return httperror.InternalServerError("Unable to update environment relations in the database", err)
}
}
}
for _, edgeGroup := range edgeGroups {
edgeGroup.TagIDs = slices.DeleteFunc(edgeGroup.TagIDs, func(t portainer.TagID) bool {
return t == tagID
@ -122,16 +123,6 @@ func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error {
}
}
for _, endpoint := range endpoints {
if (!tag.Endpoints[endpoint.ID] && !tag.EndpointGroups[endpoint.GroupID]) || !endpointutils.IsEdgeEndpoint(&endpoint) {
continue
}
if err := updateEndpointRelations(tx, endpoint, edgeGroups, edgeStacks, edgeJobs); err != nil {
return httperror.InternalServerError("Unable to update environment relations in the database", err)
}
}
err = tx.Tag().Delete(tagID)
if err != nil {
return httperror.InternalServerError("Unable to remove the tag from the database", err)
@ -140,12 +131,19 @@ func deleteTag(tx dataservices.DataStoreTx, tagID portainer.TagID) error {
return nil
}
func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack, edgeJobs []portainer.EdgeJob) error {
func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil {
if err != nil && !tx.IsErrObjectNotFound(err) {
return err
}
if endpointRelation == nil {
endpointRelation = &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: make(map[portainer.EdgeStackID]bool),
}
}
endpointGroup, err := tx.EndpointGroup().Read(endpoint.GroupID)
if err != nil {
return err
@ -159,25 +157,5 @@ func updateEndpointRelations(tx dataservices.DataStoreTx, endpoint portainer.End
endpointRelation.EdgeStacks = stacksSet
if err := tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation); err != nil {
return err
}
for _, edgeJob := range edgeJobs {
endpoints, err := edge.GetEndpointsFromEdgeGroups(edgeJob.EdgeGroups, tx)
if err != nil {
return err
}
if slices.Contains(endpoints, endpoint.ID) {
continue
}
delete(edgeJob.GroupLogsCollection, endpoint.ID)
if err := tx.EdgeJob().Update(edgeJob.ID, &edgeJob); err != nil {
return err
}
}
return nil
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
}

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