From dd0d1737b008bc755f3a8bbbf3117d4852809012 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:25:41 -0300 Subject: [PATCH] fix(performance): optimize performance for edge EE-3311 (#8040) --- api/adminmonitor/admin_monitor.go | 9 +- api/chisel/schedules.go | 9 +- api/chisel/service.go | 24 ++-- api/chisel/tunnel.go | 12 +- api/dataservices/edgestack/edgestack.go | 135 +++++++++++++++++- api/dataservices/endpoint/endpoint.go | 111 +++++++++++++- .../endpointrelation/endpointrelation.go | 24 +++- api/dataservices/interface.go | 4 + api/datastore/teststore.go | 25 +--- api/go.mod | 3 + api/go.sum | 9 ++ .../endpoint_edgestatus_inspect.go | 98 +++++++++++-- .../endpointedge_status_inspect_test.go | 12 +- api/http/handler/endpointedge/handler.go | 8 +- .../endpoints/endpoint_create_global_key.go | 14 +- api/http/handler/endpoints/filter.go | 99 +++++++------ api/http/handler/endpoints/filter_test.go | 2 +- api/http/handler/handler.go | 6 +- api/http/security/bouncer.go | 7 +- api/http/security/filter.go | 70 ++++----- api/http/server.go | 5 +- api/internal/edge/cache/cache.go | 26 ++++ api/internal/testhelpers/datastore.go | 29 ++++ 23 files changed, 577 insertions(+), 164 deletions(-) create mode 100644 api/internal/edge/cache/cache.go diff --git a/api/adminmonitor/admin_monitor.go b/api/adminmonitor/admin_monitor.go index 2b32bb9c2..2f5bfa696 100644 --- a/api/adminmonitor/admin_monitor.go +++ b/api/adminmonitor/admin_monitor.go @@ -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 } diff --git a/api/chisel/schedules.go b/api/chisel/schedules.go index 1ae4db571..f6c950bca 100644 --- a/api/chisel/schedules.go +++ b/api/chisel/schedules.go @@ -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() } diff --git a/api/chisel/service.go b/api/chisel/service.go index febbd18b9..f8a0bec5b 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -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). diff --git a/api/chisel/tunnel.go b/api/chisel/tunnel.go index 120afd8a3..41ca079b9 100644 --- a/api/chisel/tunnel.go +++ b/api/chisel/tunnel.go @@ -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() diff --git a/api/dataservices/edgestack/edgestack.go b/api/dataservices/edgestack/edgestack.go index 69f4d5ea2..52ffd23a3 100644 --- a/api/dataservices/edgestack/edgestack.go +++ b/api/dataservices/edgestack/edgestack.go @@ -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). diff --git a/api/dataservices/endpoint/endpoint.go b/api/dataservices/endpoint/endpoint.go index a798d7c97..1c367abb5 100644 --- a/api/dataservices/endpoint/endpoint.go +++ b/api/dataservices/endpoint/endpoint.go @@ -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). diff --git a/api/dataservices/endpointrelation/endpointrelation.go b/api/dataservices/endpointrelation/endpointrelation.go index 8dd05be63..7f76ffd17 100644 --- a/api/dataservices/endpointrelation/endpointrelation.go +++ b/api/dataservices/endpointrelation/endpointrelation.go @@ -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 } diff --git a/api/dataservices/interface.go b/api/dataservices/interface.go index c6b0955a4..452993ad0 100644 --- a/api/dataservices/interface.go +++ b/api/dataservices/interface.go @@ -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 diff --git a/api/datastore/teststore.go b/api/datastore/teststore.go index 8087a6143..c050537ee 100644 --- a/api/datastore/teststore.go +++ b/api/datastore/teststore.go @@ -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("") - } -} diff --git a/api/go.mod b/api/go.mod index d5bf601da..d555727ff 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 diff --git a/api/go.sum b/api/go.sum index d4d539da7..b458ae933 100644 --- a/api/go.sum +++ b/api/go.sum @@ -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= diff --git a/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go b/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go index 9c15cbce8..c96437604 100644 --- a/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestatus_inspect.go @@ -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 +} diff --git a/api/http/handler/endpointedge/endpointedge_status_inspect_test.go b/api/http/handler/endpointedge/endpointedge_status_inspect_test.go index 941fcca60..788c03cc5 100644 --- a/api/http/handler/endpointedge/endpointedge_status_inspect_test.go +++ b/api/http/handler/endpointedge/endpointedge_status_inspect_test.go @@ -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) } diff --git a/api/http/handler/endpointedge/handler.go b/api/http/handler/endpointedge/handler.go index af94f977f..35e5d5cd9 100644 --- a/api/http/handler/endpointedge/handler.go +++ b/api/http/handler/endpointedge/handler.go @@ -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 } diff --git a/api/http/handler/endpoints/endpoint_create_global_key.go b/api/http/handler/endpoints/endpoint_create_global_key.go index be82e9102..f09d276bd 100644 --- a/api/http/handler/endpoints/endpoint_create_global_key.go +++ b/api/http/handler/endpoints/endpoint_create_global_key.go @@ -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) } diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index dc9a6e74f..f4dec5eb9 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -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 { diff --git a/api/http/handler/endpoints/filter_test.go b/api/http/handler/endpoints/filter_test.go index a1f4df105..6e9ca96ff 100644 --- a/api/http/handler/endpoints/filter_test.go +++ b/api/http/handler/endpoints/filter_test.go @@ -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...)) }) } } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 2370569cb..83129d920 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -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) } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index c66d49715..ed80b229d 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -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) }) } diff --git a/api/http/security/filter.go b/api/http/security/filter.go index 549e05e18..3852bac76 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -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 } diff --git a/api/http/server.go b/api/http/server.go index 504f9d824..23b429116 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -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() diff --git a/api/internal/edge/cache/cache.go b/api/internal/edge/cache/cache.go new file mode 100644 index 000000000..26b99ebd4 --- /dev/null +++ b/api/internal/edge/cache/cache.go @@ -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)) +} diff --git a/api/internal/testhelpers/datastore.go b/api/internal/testhelpers/datastore.go index 88346bfdc..d283853ff 100644 --- a/api/internal/testhelpers/datastore.go +++ b/api/internal/testhelpers/datastore.go @@ -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 }