mirror of https://github.com/portainer/portainer
				
				
				
			fix(performance): optimize performance for edge EE-3311 (#8040)
							parent
							
								
									3d28a6f877
								
							
						
					
					
						commit
						dd0d1737b0
					
				| 
						 | 
				
			
			@ -21,7 +21,7 @@ type Monitor struct {
 | 
			
		|||
	datastore         dataservices.DataStore
 | 
			
		||||
	shutdownCtx       context.Context
 | 
			
		||||
	cancellationFunc  context.CancelFunc
 | 
			
		||||
	mu                sync.Mutex
 | 
			
		||||
	mu                sync.RWMutex
 | 
			
		||||
	adminInitDisabled bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +82,7 @@ func (m *Monitor) Stop() {
 | 
			
		|||
	if m.cancellationFunc == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.cancellationFunc()
 | 
			
		||||
	m.cancellationFunc = nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -92,12 +93,14 @@ func (m *Monitor) WasInitialized() (bool, error) {
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(users) > 0, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Monitor) WasInstanceDisabled() bool {
 | 
			
		||||
	m.mu.Lock()
 | 
			
		||||
	defer m.mu.Unlock()
 | 
			
		||||
	m.mu.RLock()
 | 
			
		||||
	defer m.mu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return m.adminInitDisabled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package chisel
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +24,8 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
 | 
			
		|||
		tunnel.Jobs[existingJobIndex] = *edgeJob
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +33,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
 | 
			
		|||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
 | 
			
		||||
	for _, tunnel := range service.tunnelDetailsMap {
 | 
			
		||||
	for endpointID, tunnel := range service.tunnelDetailsMap {
 | 
			
		||||
		n := 0
 | 
			
		||||
		for _, edgeJob := range tunnel.Jobs {
 | 
			
		||||
			if edgeJob.ID != edgeJobID {
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +43,8 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		tunnel.Jobs = tunnel.Jobs[:n]
 | 
			
		||||
 | 
			
		||||
		cache.Del(endpointID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
| 
						 | 
				
			
			@ -59,5 +64,7 @@ func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointI
 | 
			
		|||
 | 
			
		||||
	tunnel.Jobs = tunnel.Jobs[:n]
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -206,15 +206,23 @@ func (service *Service) checkTunnels() {
 | 
			
		|||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	for key, tunnel := range service.tunnelDetailsMap {
 | 
			
		||||
		if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tunnels[key] = *tunnel
 | 
			
		||||
	}
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	for endpointID, tunnel := range tunnels {
 | 
			
		||||
		if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		elapsed := time.Since(tunnel.LastActivity)
 | 
			
		||||
		log.Debug().
 | 
			
		||||
			Int("endpoint_id", int(endpointID)).
 | 
			
		||||
| 
						 | 
				
			
			@ -222,9 +230,7 @@ func (service *Service) checkTunnels() {
 | 
			
		|||
			Float64("status_time_seconds", elapsed.Seconds()).
 | 
			
		||||
			Msg("environment tunnel monitoring")
 | 
			
		||||
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
 | 
			
		||||
			continue
 | 
			
		||||
		} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
 | 
			
		||||
			log.Debug().
 | 
			
		||||
				Int("endpoint_id", int(endpointID)).
 | 
			
		||||
				Str("status", tunnel.Status).
 | 
			
		||||
| 
						 | 
				
			
			@ -233,9 +239,7 @@ func (service *Service) checkTunnels() {
 | 
			
		|||
				Msg("REQUIRED state timeout exceeded")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
 | 
			
		||||
			continue
 | 
			
		||||
		} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
 | 
			
		||||
		if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
 | 
			
		||||
			log.Debug().
 | 
			
		||||
				Int("endpoint_id", int(endpointID)).
 | 
			
		||||
				Str("status", tunnel.Status).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,9 +7,11 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/dchest/uniuri"
 | 
			
		||||
	"github.com/portainer/libcrypto"
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
 | 
			
		||||
	"github.com/dchest/uniuri"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
| 
						 | 
				
			
			@ -49,6 +51,8 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
 | 
			
		|||
 | 
			
		||||
	service.tunnelDetailsMap[endpointID] = tunnel
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	return tunnel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -99,6 +103,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
 | 
			
		|||
	tunnel.Credentials = ""
 | 
			
		||||
	tunnel.LastActivity = time.Now()
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
 | 
			
		||||
| 
						 | 
				
			
			@ -121,6 +127,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
 | 
			
		|||
	service.ProxyManager.DeleteEndpointProxy(endpointID)
 | 
			
		||||
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
 | 
			
		||||
| 
						 | 
				
			
			@ -129,6 +137,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
 | 
			
		|||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
 | 
			
		||||
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
 | 
			
		||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
 | 
			
		||||
	defer cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	tunnel := service.getTunnelDetails(endpointID)
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,10 @@ package edgestack
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +18,8 @@ const (
 | 
			
		|||
// Service represents a service for managing Edge stack data.
 | 
			
		||||
type Service struct {
 | 
			
		||||
	connection portainer.Connection
 | 
			
		||||
	idxVersion map[portainer.EdgeStackID]int
 | 
			
		||||
	mu         sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (service *Service) BucketName() string {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,9 +33,21 @@ func NewService(connection portainer.Connection) (*Service, error) {
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Service{
 | 
			
		||||
	s := &Service{
 | 
			
		||||
		connection: connection,
 | 
			
		||||
	}, nil
 | 
			
		||||
		idxVersion: make(map[portainer.EdgeStackID]int),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	es, err := s.EdgeStacks()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, e := range es {
 | 
			
		||||
		s.idxVersion[e.ID] = e.Version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EdgeStacks returns an array containing all edge stacks
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +58,6 @@ func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
 | 
			
		|||
		BucketName,
 | 
			
		||||
		&portainer.EdgeStack{},
 | 
			
		||||
		func(obj interface{}) (interface{}, error) {
 | 
			
		||||
			//var tag portainer.Tag
 | 
			
		||||
			stack, ok := obj.(*portainer.EdgeStack)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
 | 
			
		||||
| 
						 | 
				
			
			@ -70,22 +85,77 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
 | 
			
		|||
	return &stack, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
 | 
			
		||||
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
 | 
			
		||||
	service.mu.RLock()
 | 
			
		||||
	v, ok := service.idxVersion[ID]
 | 
			
		||||
	service.mu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return v, ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateEdgeStack saves an Edge stack object to db.
 | 
			
		||||
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
 | 
			
		||||
 | 
			
		||||
	edgeStack.ID = id
 | 
			
		||||
 | 
			
		||||
	return service.connection.CreateObjectWithId(
 | 
			
		||||
	err := service.connection.CreateObjectWithId(
 | 
			
		||||
		BucketName,
 | 
			
		||||
		int(edgeStack.ID),
 | 
			
		||||
		edgeStack,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	service.idxVersion[id] = edgeStack.Version
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	for endpointID := range edgeStack.Status {
 | 
			
		||||
		cache.Del(endpointID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use UpdateEdgeStackFunc instead.
 | 
			
		||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	defer service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	prevEdgeStack, err := service.EdgeStack(ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(ID))
 | 
			
		||||
	return service.connection.UpdateObject(BucketName, identifier, edgeStack)
 | 
			
		||||
 | 
			
		||||
	err = service.connection.UpdateObject(BucketName, identifier, edgeStack)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.idxVersion[ID] = edgeStack.Version
 | 
			
		||||
 | 
			
		||||
	// Invalidate cache for removed environments
 | 
			
		||||
	for endpointID := range prevEdgeStack.Status {
 | 
			
		||||
		if _, ok := edgeStack.Status[endpointID]; !ok {
 | 
			
		||||
			cache.Del(endpointID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Invalidate cache when version changes and for added environments
 | 
			
		||||
	for endpointID := range edgeStack.Status {
 | 
			
		||||
		if prevEdgeStack.Version == edgeStack.Version {
 | 
			
		||||
			if _, ok := prevEdgeStack.Status[endpointID]; ok {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cache.Del(endpointID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
 | 
			
		||||
| 
						 | 
				
			
			@ -93,15 +163,66 @@ func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc
 | 
			
		|||
	id := service.connection.ConvertToKey(int(ID))
 | 
			
		||||
	edgeStack := &portainer.EdgeStack{}
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	defer service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
 | 
			
		||||
		prevEndpoints := make(map[portainer.EndpointID]struct{}, len(edgeStack.Status))
 | 
			
		||||
		for endpointID := range edgeStack.Status {
 | 
			
		||||
			if _, ok := edgeStack.Status[endpointID]; !ok {
 | 
			
		||||
				prevEndpoints[endpointID] = struct{}{}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		updateFunc(edgeStack)
 | 
			
		||||
 | 
			
		||||
		prevVersion := service.idxVersion[ID]
 | 
			
		||||
		service.idxVersion[ID] = edgeStack.Version
 | 
			
		||||
 | 
			
		||||
		// Invalidate cache for removed environments
 | 
			
		||||
		for endpointID := range prevEndpoints {
 | 
			
		||||
			if _, ok := edgeStack.Status[endpointID]; !ok {
 | 
			
		||||
				cache.Del(endpointID)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Invalidate cache when version changes and for added environments
 | 
			
		||||
		for endpointID := range edgeStack.Status {
 | 
			
		||||
			if prevVersion == edgeStack.Version {
 | 
			
		||||
				if _, ok := prevEndpoints[endpointID]; ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cache.Del(endpointID)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteEdgeStack deletes an Edge stack.
 | 
			
		||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	defer service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	edgeStack, err := service.EdgeStack(ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(ID))
 | 
			
		||||
	return service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
 | 
			
		||||
	err = service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	delete(service.idxVersion, ID)
 | 
			
		||||
 | 
			
		||||
	for endpointID := range edgeStack.Status {
 | 
			
		||||
		cache.Del(endpointID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,8 +2,11 @@ package endpoint
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +19,9 @@ const (
 | 
			
		|||
// Service represents a service for managing environment(endpoint) data.
 | 
			
		||||
type Service struct {
 | 
			
		||||
	connection portainer.Connection
 | 
			
		||||
	mu         sync.RWMutex
 | 
			
		||||
	idxEdgeID  map[string]portainer.EndpointID
 | 
			
		||||
	heartbeats sync.Map
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (service *Service) BucketName() string {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,9 +35,25 @@ func NewService(connection portainer.Connection) (*Service, error) {
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Service{
 | 
			
		||||
	s := &Service{
 | 
			
		||||
		connection: connection,
 | 
			
		||||
	}, nil
 | 
			
		||||
		idxEdgeID:  make(map[string]portainer.EndpointID),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	es, err := s.Endpoints()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, e := range es {
 | 
			
		||||
		if len(e.EdgeID) > 0 {
 | 
			
		||||
			s.idxEdgeID[e.EdgeID] = e.ID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s.heartbeats.Store(e.ID, e.LastCheckInDate)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Endpoint returns an environment(endpoint) by ID.
 | 
			
		||||
| 
						 | 
				
			
			@ -44,19 +66,54 @@ func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
 | 
			
		|||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
 | 
			
		||||
 | 
			
		||||
	return &endpoint, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateEndpoint updates an environment(endpoint).
 | 
			
		||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(ID))
 | 
			
		||||
	return service.connection.UpdateObject(BucketName, identifier, endpoint)
 | 
			
		||||
 | 
			
		||||
	err := service.connection.UpdateObject(BucketName, identifier, endpoint)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	if len(endpoint.EdgeID) > 0 {
 | 
			
		||||
		service.idxEdgeID[endpoint.EdgeID] = ID
 | 
			
		||||
	}
 | 
			
		||||
	service.heartbeats.Store(ID, endpoint.LastCheckInDate)
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	cache.Del(endpoint.ID)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteEndpoint deletes an environment(endpoint).
 | 
			
		||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(ID))
 | 
			
		||||
	return service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
 | 
			
		||||
	err := service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	for edgeID, endpointID := range service.idxEdgeID {
 | 
			
		||||
		if endpointID == ID {
 | 
			
		||||
			delete(service.idxEdgeID, edgeID)
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	service.heartbeats.Delete(ID)
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	cache.Del(ID)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Endpoints return an array containing all the environments(endpoints).
 | 
			
		||||
| 
						 | 
				
			
			@ -78,12 +135,54 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
 | 
			
		|||
			return &portainer.Endpoint{}, nil
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
	return endpoints, err
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return endpoints, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, e := range endpoints {
 | 
			
		||||
		t, _ := service.Heartbeat(e.ID)
 | 
			
		||||
		endpoints[i].LastCheckInDate = t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return endpoints, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
 | 
			
		||||
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
 | 
			
		||||
	service.mu.RLock()
 | 
			
		||||
	endpointID, ok := service.idxEdgeID[edgeID]
 | 
			
		||||
	service.mu.RUnlock()
 | 
			
		||||
 | 
			
		||||
	return endpointID, ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
 | 
			
		||||
	if t, ok := service.heartbeats.Load(endpointID); ok {
 | 
			
		||||
		return t.(int64), true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
 | 
			
		||||
	service.heartbeats.Store(endpointID, time.Now().Unix())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
 | 
			
		||||
func (service *Service) Create(endpoint *portainer.Endpoint) error {
 | 
			
		||||
	return service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
 | 
			
		||||
	err := service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	service.mu.Lock()
 | 
			
		||||
	if len(endpoint.EdgeID) > 0 {
 | 
			
		||||
		service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
 | 
			
		||||
	}
 | 
			
		||||
	service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
 | 
			
		||||
	service.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -71,17 +72,26 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
 | 
			
		|||
 | 
			
		||||
// CreateEndpointRelation saves endpointRelation
 | 
			
		||||
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
 | 
			
		||||
	return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
 | 
			
		||||
	err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
 | 
			
		||||
	cache.Del(endpointRelation.EndpointID)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
 | 
			
		||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(EndpointID))
 | 
			
		||||
	return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
 | 
			
		||||
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(endpointID))
 | 
			
		||||
	err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
 | 
			
		||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(EndpointID))
 | 
			
		||||
	return service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
 | 
			
		||||
	identifier := service.connection.ConvertToKey(int(endpointID))
 | 
			
		||||
	err := service.connection.DeleteObject(BucketName, identifier)
 | 
			
		||||
	cache.Del(endpointID)
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,6 +87,7 @@ type (
 | 
			
		|||
	EdgeStackService interface {
 | 
			
		||||
		EdgeStacks() ([]portainer.EdgeStack, error)
 | 
			
		||||
		EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
 | 
			
		||||
		EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
 | 
			
		||||
		Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
 | 
			
		||||
		UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
 | 
			
		||||
		UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +99,9 @@ type (
 | 
			
		|||
	// EndpointService represents a service for managing environment(endpoint) data
 | 
			
		||||
	EndpointService interface {
 | 
			
		||||
		Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
 | 
			
		||||
		EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
 | 
			
		||||
		Heartbeat(endpointID portainer.EndpointID) (int64, bool)
 | 
			
		||||
		UpdateHeartbeat(endpointID portainer.EndpointID)
 | 
			
		||||
		Endpoints() ([]portainer.Endpoint, error)
 | 
			
		||||
		Create(endpoint *portainer.Endpoint) error
 | 
			
		||||
		UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,32 +8,23 @@ import (
 | 
			
		|||
	"github.com/portainer/portainer/api/database/models"
 | 
			
		||||
	"github.com/portainer/portainer/api/filesystem"
 | 
			
		||||
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var errTempDir = errors.New("can't create a temp dir")
 | 
			
		||||
 | 
			
		||||
func (store *Store) GetConnection() portainer.Connection {
 | 
			
		||||
	return store.connection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func MustNewTestStore(t *testing.T, init, secure bool) (bool, *Store, func()) {
 | 
			
		||||
func MustNewTestStore(t testing.TB, init, secure bool) (bool, *Store, func()) {
 | 
			
		||||
	newStore, store, teardown, err := NewTestStore(t, init, secure)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !errors.Is(err, errTempDir) {
 | 
			
		||||
			if teardown != nil {
 | 
			
		||||
				teardown()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Fatal().Err(err).Msg("")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newStore, store, teardown
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error) {
 | 
			
		||||
func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error) {
 | 
			
		||||
	// Creates unique temp directory in a concurrency friendly manner.
 | 
			
		||||
	storePath := t.TempDir()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -78,15 +69,11 @@ func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error)
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	teardown := func() {
 | 
			
		||||
		teardown(store)
 | 
			
		||||
		err := store.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal().Err(err).Msg("")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newStore, store, teardown, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func teardown(store *Store) {
 | 
			
		||||
	err := store.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal().Err(err).Msg("")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ go 1.18
 | 
			
		|||
require (
 | 
			
		||||
	github.com/Masterminds/semver v1.5.0
 | 
			
		||||
	github.com/Microsoft/go-winio v0.5.1
 | 
			
		||||
	github.com/VictoriaMetrics/fastcache v1.12.0
 | 
			
		||||
	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2 v1.11.1
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/credentials v1.6.2
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +67,7 @@ require (
 | 
			
		|||
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
 | 
			
		||||
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
 | 
			
		||||
	github.com/aws/smithy-go v1.9.0 // indirect
 | 
			
		||||
	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 | 
			
		||||
	github.com/davecgh/go-spew v1.1.1 // indirect
 | 
			
		||||
	github.com/docker/distribution v2.8.1+incompatible // indirect
 | 
			
		||||
	github.com/docker/go-connections v0.4.0 // indirect
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +88,7 @@ require (
 | 
			
		|||
	github.com/go-playground/universal-translator v0.18.0 // indirect
 | 
			
		||||
	github.com/gogo/protobuf v1.3.2 // indirect
 | 
			
		||||
	github.com/golang/protobuf v1.5.2 // indirect
 | 
			
		||||
	github.com/golang/snappy v0.0.4 // indirect
 | 
			
		||||
	github.com/google/gnostic v0.5.7-v3refs // indirect
 | 
			
		||||
	github.com/google/gofuzz v1.2.0 // indirect
 | 
			
		||||
	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,12 +45,16 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
 | 
			
		|||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
			
		||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 | 
			
		||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
			
		||||
github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE=
 | 
			
		||||
github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8=
 | 
			
		||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 | 
			
		||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
 | 
			
		||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 | 
			
		||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 | 
			
		||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
 | 
			
		||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
 | 
			
		||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 | 
			
		||||
| 
						 | 
				
			
			@ -78,6 +82,8 @@ github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm
 | 
			
		|||
github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU=
 | 
			
		||||
github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM=
 | 
			
		||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 | 
			
		||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 | 
			
		||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 | 
			
		||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 | 
			
		||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 | 
			
		||||
| 
						 | 
				
			
			@ -199,6 +205,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 | 
			
		|||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 | 
			
		||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
			
		||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 | 
			
		||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
			
		||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
			
		||||
github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54=
 | 
			
		||||
| 
						 | 
				
			
			@ -543,6 +551,7 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
			
		|||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
 | 
			
		||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,23 @@
 | 
			
		|||
package endpointedge
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash/fnv"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	httperror "github.com/portainer/libhttp/error"
 | 
			
		||||
	"github.com/portainer/libhttp/request"
 | 
			
		||||
	"github.com/portainer/libhttp/response"
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/http/middlewares"
 | 
			
		||||
	"github.com/portainer/portainer/api/internal/edge/cache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type stackStatusResponse struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -64,9 +70,27 @@ type endpointEdgeStatusInspectResponse struct {
 | 
			
		|||
// @failure 500 "Server error"
 | 
			
		||||
// @router /endpoints/{id}/edge/status [get]
 | 
			
		||||
func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
 | 
			
		||||
	endpoint, err := middlewares.FetchEndpoint(r)
 | 
			
		||||
	endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return httperror.BadRequest("Unable to find an environment on request context", err)
 | 
			
		||||
		return httperror.BadRequest("Invalid environment identifier route variable", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cachedResp := handler.respondFromCache(w, r, portainer.EndpointID(endpointID))
 | 
			
		||||
	if cachedResp {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
 | 
			
		||||
		return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if handler.DataStore.IsErrObjectNotFound(err) {
 | 
			
		||||
			return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +153,7 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
 | 
			
		|||
	}
 | 
			
		||||
	statusResponse.Stacks = edgeStacksStatus
 | 
			
		||||
 | 
			
		||||
	return response.JSON(w, statusResponse)
 | 
			
		||||
	return cacheResponse(w, endpoint.ID, statusResponse)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -191,17 +215,75 @@ func (handler *Handler) buildEdgeStacks(endpointID portainer.EndpointID) ([]stac
 | 
			
		|||
 | 
			
		||||
	edgeStacksStatus := []stackStatusResponse{}
 | 
			
		||||
	for stackID := range relation.EdgeStacks {
 | 
			
		||||
		stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		version, ok := handler.DataStore.EdgeStack().EdgeStackVersion(stackID)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return nil, httperror.InternalServerError("Unable to retrieve edge stack from the database", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stackStatus := stackStatusResponse{
 | 
			
		||||
			ID:      stack.ID,
 | 
			
		||||
			Version: stack.Version,
 | 
			
		||||
			ID:      stackID,
 | 
			
		||||
			Version: version,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		edgeStacksStatus = append(edgeStacksStatus, stackStatus)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return edgeStacksStatus, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func cacheResponse(w http.ResponseWriter, endpointID portainer.EndpointID, statusResponse endpointEdgeStatusInspectResponse) *httperror.HandlerError {
 | 
			
		||||
	rr := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
	httpErr := response.JSON(rr, statusResponse)
 | 
			
		||||
	if httpErr != nil {
 | 
			
		||||
		return httpErr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h := fnv.New32a()
 | 
			
		||||
	h.Write(rr.Body.Bytes())
 | 
			
		||||
	etag := strconv.FormatUint(uint64(h.Sum32()), 16)
 | 
			
		||||
 | 
			
		||||
	cache.Set(endpointID, []byte(etag))
 | 
			
		||||
 | 
			
		||||
	resp := rr.Result()
 | 
			
		||||
 | 
			
		||||
	for k, vs := range resp.Header {
 | 
			
		||||
		for _, v := range vs {
 | 
			
		||||
			w.Header().Add(k, v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("ETag", etag)
 | 
			
		||||
	io.Copy(w, resp.Body)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (handler *Handler) respondFromCache(w http.ResponseWriter, r *http.Request, endpointID portainer.EndpointID) bool {
 | 
			
		||||
	inmHeader := r.Header.Get("If-None-Match")
 | 
			
		||||
	etags := strings.Split(inmHeader, ",")
 | 
			
		||||
 | 
			
		||||
	if len(inmHeader) == 0 || etags[0] == "" {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cachedETag, ok := cache.Get(endpointID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, etag := range etags {
 | 
			
		||||
		if !bytes.Equal([]byte(etag), cachedETag) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		handler.DataStore.Endpoint().UpdateHeartbeat(endpointID)
 | 
			
		||||
 | 
			
		||||
		w.Header().Set("ETag", etag)
 | 
			
		||||
		w.WriteHeader(http.StatusNotModified)
 | 
			
		||||
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,7 +158,7 @@ func TestMissingEdgeIdentifier(t *testing.T) {
 | 
			
		|||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpointID), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal("request error:", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -185,7 +185,7 @@ func TestWithEndpoints(t *testing.T) {
 | 
			
		|||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
 | 
			
		||||
		req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", test.endpoint.ID), nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal("request error:", err)
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +231,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	time.Sleep(1 * time.Second)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal("request error:", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +279,7 @@ func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
 | 
			
		|||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal("request error:", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -348,7 +348,7 @@ func TestEdgeStackStatus(t *testing.T) {
 | 
			
		|||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal("request error:", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -418,7 +418,7 @@ func TestEdgeJobsResponse(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal("request error:", err)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,14 +31,16 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
 | 
			
		|||
		ReverseTunnelService: reverseTunnelService,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	endpointRouter := h.PathPrefix("/{id}").Subrouter()
 | 
			
		||||
	h.Handle("/api/endpoints/{id}/edge/status", bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
 | 
			
		||||
 | 
			
		||||
	endpointRouter := h.PathPrefix("/api/endpoints/{id}").Subrouter()
 | 
			
		||||
	endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
 | 
			
		||||
 | 
			
		||||
	endpointRouter.PathPrefix("/edge/status").Handler(
 | 
			
		||||
		bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
 | 
			
		||||
	endpointRouter.PathPrefix("/edge/stacks/{stackId}").Handler(
 | 
			
		||||
		bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
 | 
			
		||||
 | 
			
		||||
	endpointRouter.PathPrefix("/edge/jobs/{jobID}/logs").Handler(
 | 
			
		||||
		bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
 | 
			
		||||
 | 
			
		||||
	return h
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,16 +28,10 @@ func (handler *Handler) endpointCreateGlobalKey(w http.ResponseWriter, r *http.R
 | 
			
		|||
 | 
			
		||||
	// Search for existing endpoints for the given edgeID
 | 
			
		||||
 | 
			
		||||
	endpoints, err := handler.DataStore.Endpoint().Endpoints()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return httperror.InternalServerError("Unable to retrieve the endpoints from the database", err)
 | 
			
		||||
	endpointID, ok := handler.DataStore.Endpoint().EndpointIDByEdgeID(edgeID)
 | 
			
		||||
	if ok {
 | 
			
		||||
		return response.JSON(w, endpointCreateGlobalKeyResponse{endpointID})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if endpoint.EdgeID == edgeID {
 | 
			
		||||
			return response.JSON(w, endpointCreateGlobalKeyResponse{endpoint.ID})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return httperror.NotFound("Unable to find the endpoint in the database", err)
 | 
			
		||||
	return httperror.NotFound("Unable to find the endpoint in the database", nil)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,38 +153,39 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
 | 
			
		||||
		if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		status := endpoint.Status
 | 
			
		||||
		if endpointutils.IsEdgeEndpoint(&endpoint) {
 | 
			
		||||
| 
						 | 
				
			
			@ -205,11 +206,12 @@ func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portai
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if slices.Contains(statuses, status) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
 | 
			
		||||
| 
						 | 
				
			
			@ -226,6 +228,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
 | 
			
		|||
	} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tag := range tags {
 | 
			
		||||
		if strings.Contains(strings.ToLower(tag), searchCriteria) {
 | 
			
		||||
			return true
 | 
			
		||||
| 
						 | 
				
			
			@ -241,6 +244,7 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
 | 
			
		|||
			if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tags := convertTagIDsToTags(tagsMap, group.TagIDs)
 | 
			
		||||
			for _, tag := range tags {
 | 
			
		||||
				if strings.Contains(strings.ToLower(tag), searchCriteria) {
 | 
			
		||||
| 
						 | 
				
			
			@ -254,30 +258,32 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	typeSet := map[portainer.EndpointType]bool{}
 | 
			
		||||
	for _, endpointType := range endpointTypes {
 | 
			
		||||
		typeSet[portainer.EndpointType(endpointType)] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if typeSet[endpoint.Type] {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
 | 
			
		||||
| 
						 | 
				
			
			@ -293,19 +299,21 @@ func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, u
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
 | 
			
		||||
	tags := make([]string, 0)
 | 
			
		||||
	tags := make([]string, 0, len(tagIDs))
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range tagIDs {
 | 
			
		||||
		tags = append(tags, tagsMap[tagID])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tags
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
 | 
			
		||||
		endpointMatched := false
 | 
			
		||||
 | 
			
		||||
		if partialMatch {
 | 
			
		||||
			endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -313,27 +321,33 @@ func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if endpointMatched {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
 | 
			
		||||
	tagSet := make(map[portainer.TagID]bool)
 | 
			
		||||
	tagSet := make(map[portainer.TagID]bool, len(tagIDs))
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range tagIDs {
 | 
			
		||||
		tagSet[tagID] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range endpoint.TagIDs {
 | 
			
		||||
		if tagSet[tagID] {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range endpointGroup.TagIDs {
 | 
			
		||||
		if tagSet[tagID] {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -342,34 +356,37 @@ func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.
 | 
			
		|||
	for _, tagID := range tagIDs {
 | 
			
		||||
		missingTags[tagID] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range endpoint.TagIDs {
 | 
			
		||||
		if missingTags[tagID] {
 | 
			
		||||
			delete(missingTags, tagID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tagID := range endpointGroup.TagIDs {
 | 
			
		||||
		if missingTags[tagID] {
 | 
			
		||||
			delete(missingTags, tagID)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(missingTags) == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	idsSet := make(map[portainer.EndpointID]bool)
 | 
			
		||||
	idsSet := make(map[portainer.EndpointID]bool, len(ids))
 | 
			
		||||
	for _, id := range ids {
 | 
			
		||||
		idsSet[id] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if idsSet[endpoint.ID] {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -378,25 +395,27 @@ func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portai
 | 
			
		|||
		return endpoints
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if endpoint.Name == name {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := make([]portainer.Endpoint, 0)
 | 
			
		||||
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		if predicate(endpoint) {
 | 
			
		||||
			filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getArrayQueryParameter(r *http.Request, parameter string) []string {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -132,7 +132,7 @@ func Test_Filter_edgeDeviceFilter(t *testing.T) {
 | 
			
		|||
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.title, func(t *testing.T) {
 | 
			
		||||
			runTest(t, test, handler, endpoints)
 | 
			
		||||
			runTest(t, test, handler, append([]portainer.Endpoint{}, endpoints...))
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -161,6 +161,8 @@ type Handler struct {
 | 
			
		|||
// ServeHTTP delegates a request to the appropriate subhandler.
 | 
			
		||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	switch {
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/endpoints") && strings.Contains(r.URL.Path, "/edge/"):
 | 
			
		||||
		h.EndpointEdgeHandler.ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/auth"):
 | 
			
		||||
		http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/backup"):
 | 
			
		||||
| 
						 | 
				
			
			@ -175,8 +177,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
		http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/edge_jobs"):
 | 
			
		||||
		http.StripPrefix("/api", h.EdgeJobsHandler).ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
 | 
			
		||||
		http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/edge_templates"):
 | 
			
		||||
		http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r)
 | 
			
		||||
	case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
 | 
			
		||||
| 
						 | 
				
			
			@ -200,8 +200,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		|||
			http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
 | 
			
		||||
		case strings.Contains(r.URL.Path, "/agent/"):
 | 
			
		||||
			http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
 | 
			
		||||
		case strings.Contains(r.URL.Path, "/edge/"):
 | 
			
		||||
			http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
 | 
			
		||||
		default:
 | 
			
		||||
			http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,8 +49,7 @@ func NewRequestBouncer(dataStore dataservices.DataStore, jwtService dataservices
 | 
			
		|||
// PublicAccess defines a security check for public API environments(endpoints).
 | 
			
		||||
// No authentication is required to access these environments(endpoints).
 | 
			
		||||
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
 | 
			
		||||
	h = mwSecureHeaders(h)
 | 
			
		||||
	return h
 | 
			
		||||
	return mwSecureHeaders(h)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AdminAccess defines a security check for API environments(endpoints) that require an authorization check.
 | 
			
		||||
| 
						 | 
				
			
			@ -375,8 +374,8 @@ func extractAPIKey(r *http.Request) (apikey string, ok bool) {
 | 
			
		|||
// mwSecureHeaders provides secure headers middleware for handlers.
 | 
			
		||||
func mwSecureHeaders(next http.Handler) http.Handler {
 | 
			
		||||
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		w.Header().Add("X-XSS-Protection", "1; mode=block")
 | 
			
		||||
		w.Header().Add("X-Content-Type-Options", "nosniff")
 | 
			
		||||
		w.Header().Set("X-XSS-Protection", "1; mode=block")
 | 
			
		||||
		w.Header().Set("X-Content-Type-Options", "nosniff")
 | 
			
		||||
		next.ServeHTTP(w, r)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,26 +11,28 @@ func FilterUserTeams(teams []portainer.Team, context *RestrictedRequestContext)
 | 
			
		|||
		return teams
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teamsAccessableToUser := make([]portainer.Team, 0)
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, membership := range context.UserMemberships {
 | 
			
		||||
		for _, team := range teams {
 | 
			
		||||
			if team.ID == membership.TeamID {
 | 
			
		||||
				teamsAccessableToUser = append(teamsAccessableToUser, team)
 | 
			
		||||
				teams[n] = team
 | 
			
		||||
				n++
 | 
			
		||||
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return teamsAccessableToUser
 | 
			
		||||
	return teams[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterLeaderTeams filters teams based on user role.
 | 
			
		||||
// Team leaders only have access to team they lead.
 | 
			
		||||
func FilterLeaderTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
 | 
			
		||||
	filteredTeams := []portainer.Team{}
 | 
			
		||||
	n := 0
 | 
			
		||||
 | 
			
		||||
	if !context.IsTeamLeader {
 | 
			
		||||
		return filteredTeams
 | 
			
		||||
		return teams[:n]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	leaderSet := map[portainer.TeamID]bool{}
 | 
			
		||||
| 
						 | 
				
			
			@ -42,11 +44,12 @@ func FilterLeaderTeams(teams []portainer.Team, context *RestrictedRequestContext
 | 
			
		|||
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		if leaderSet[team.ID] {
 | 
			
		||||
			filteredTeams = append(filteredTeams, team)
 | 
			
		||||
			teams[n] = team
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredTeams
 | 
			
		||||
	return teams[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterUsers filters users based on user role.
 | 
			
		||||
| 
						 | 
				
			
			@ -56,14 +59,15 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
 | 
			
		|||
		return users
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nonAdmins := make([]portainer.User, 0)
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, user := range users {
 | 
			
		||||
		if user.Role != portainer.AdministratorRole {
 | 
			
		||||
			nonAdmins = append(nonAdmins, user)
 | 
			
		||||
			users[n] = user
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nonAdmins
 | 
			
		||||
	return users[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterRegistries filters registries based on user role and team memberships.
 | 
			
		||||
| 
						 | 
				
			
			@ -73,52 +77,53 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
 | 
			
		|||
		return registries
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filteredRegistries := []portainer.Registry{}
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, registry := range registries {
 | 
			
		||||
		if AuthorizedRegistryAccess(®istry, user, teamMemberships, endpointID) {
 | 
			
		||||
			filteredRegistries = append(filteredRegistries, registry)
 | 
			
		||||
			registries[n] = registry
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredRegistries
 | 
			
		||||
	return registries[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
 | 
			
		||||
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
 | 
			
		||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
 | 
			
		||||
	filteredEndpoints := endpoints
 | 
			
		||||
	if context.IsAdmin {
 | 
			
		||||
		return endpoints
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !context.IsAdmin {
 | 
			
		||||
		filteredEndpoints = make([]portainer.Endpoint, 0)
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, endpoint := range endpoints {
 | 
			
		||||
		endpointGroup := getAssociatedGroup(&endpoint, groups)
 | 
			
		||||
 | 
			
		||||
		for _, endpoint := range endpoints {
 | 
			
		||||
			endpointGroup := getAssociatedGroup(&endpoint, groups)
 | 
			
		||||
 | 
			
		||||
			if AuthorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
 | 
			
		||||
				filteredEndpoints = append(filteredEndpoints, endpoint)
 | 
			
		||||
			}
 | 
			
		||||
		if AuthorizedEndpointAccess(&endpoint, endpointGroup, context.UserID, context.UserMemberships) {
 | 
			
		||||
			endpoints[n] = endpoint
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpoints
 | 
			
		||||
	return endpoints[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
 | 
			
		||||
// Non administrator users only have access to authorized environment(endpoint) groups.
 | 
			
		||||
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
 | 
			
		||||
	filteredEndpointGroups := endpointGroups
 | 
			
		||||
	if context.IsAdmin {
 | 
			
		||||
		return endpointGroups
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !context.IsAdmin {
 | 
			
		||||
		filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
 | 
			
		||||
 | 
			
		||||
		for _, group := range endpointGroups {
 | 
			
		||||
			if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
 | 
			
		||||
				filteredEndpointGroups = append(filteredEndpointGroups, group)
 | 
			
		||||
			}
 | 
			
		||||
	n := 0
 | 
			
		||||
	for _, group := range endpointGroups {
 | 
			
		||||
		if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
 | 
			
		||||
			endpointGroups[n] = group
 | 
			
		||||
			n++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filteredEndpointGroups
 | 
			
		||||
	return endpointGroups[:n]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getAssociatedGroup(endpoint *portainer.Endpoint, groups []portainer.EndpointGroup) *portainer.EndpointGroup {
 | 
			
		||||
| 
						 | 
				
			
			@ -127,5 +132,6 @@ func getAssociatedGroup(endpoint *portainer.Endpoint, groups []portainer.Endpoin
 | 
			
		|||
			return &group
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -341,8 +341,9 @@ func (server *Server) Start() error {
 | 
			
		|||
 | 
			
		||||
	log.Info().Str("bind_address", server.BindAddressHTTPS).Msg("starting HTTPS server")
 | 
			
		||||
	httpsServer := &http.Server{
 | 
			
		||||
		Addr:    server.BindAddressHTTPS,
 | 
			
		||||
		Handler: handler,
 | 
			
		||||
		Addr:         server.BindAddressHTTPS,
 | 
			
		||||
		Handler:      handler,
 | 
			
		||||
		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // Disable HTTP/2
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	httpsServer.TLSConfig = crypto.CreateServerTLSConfiguration()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
package cache
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/VictoriaMetrics/fastcache"
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var c = fastcache.New(1)
 | 
			
		||||
 | 
			
		||||
func key(k portainer.EndpointID) []byte {
 | 
			
		||||
	return []byte(strconv.Itoa(int(k)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Set(k portainer.EndpointID, v []byte) {
 | 
			
		||||
	c.Set(key(k), v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Get(k portainer.EndpointID) ([]byte, bool) {
 | 
			
		||||
	return c.HasGet(nil, key(k))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Del(k portainer.EndpointID) {
 | 
			
		||||
	c.Del(key(k))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@ package testhelpers
 | 
			
		|||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	portainer "github.com/portainer/portainer/api"
 | 
			
		||||
	"github.com/portainer/portainer/api/dataservices"
 | 
			
		||||
| 
						 | 
				
			
			@ -228,6 +229,34 @@ func (s *stubEndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endp
 | 
			
		|||
	return nil, errors.ErrObjectNotFound
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubEndpointService) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
 | 
			
		||||
	for _, endpoint := range s.endpoints {
 | 
			
		||||
		if endpoint.EdgeID == edgeID {
 | 
			
		||||
			return endpoint.ID, true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubEndpointService) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
 | 
			
		||||
	for i, endpoint := range s.endpoints {
 | 
			
		||||
		if endpoint.ID == endpointID {
 | 
			
		||||
			return s.endpoints[i].LastCheckInDate, true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubEndpointService) UpdateHeartbeat(endpointID portainer.EndpointID) {
 | 
			
		||||
	for i, endpoint := range s.endpoints {
 | 
			
		||||
		if endpoint.ID == endpointID {
 | 
			
		||||
			s.endpoints[i].LastCheckInDate = time.Now().Unix()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *stubEndpointService) Endpoints() ([]portainer.Endpoint, error) {
 | 
			
		||||
	return s.endpoints, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue