From e9ce3d22134068abf92c031d92cefdd2116d30aa Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:19:07 -0300 Subject: [PATCH] fix(endpointedge): optimize buildSchedules() BE-12099 (#955) --- .../endpointedge_status_inspect.go | 19 +++-- api/internal/edge/edgegroup.go | 11 ++- api/internal/edge/endpoint.go | 28 +++---- api/internal/edge/endpoint_test.go | 82 +++++++++++++++++++ 4 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 api/internal/edge/endpoint_test.go diff --git a/api/http/handler/endpointedge/endpointedge_status_inspect.go b/api/http/handler/endpointedge/endpointedge_status_inspect.go index 9bd341561..9c4e4bbe1 100644 --- a/api/http/handler/endpointedge/endpointedge_status_inspect.go +++ b/api/http/handler/endpointedge/endpointedge_status_inspect.go @@ -170,7 +170,7 @@ func (handler *Handler) inspectStatus(tx dataservices.DataStoreTx, r *http.Reque Credentials: tunnel.Credentials, } - schedules, handlerErr := handler.buildSchedules(tx, endpoint.ID) + schedules, handlerErr := handler.buildSchedules(tx, endpoint) if handlerErr != nil { return nil, handlerErr } @@ -208,7 +208,7 @@ func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) { } } -func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID portainer.EndpointID) ([]edgeJobResponse, *httperror.HandlerError) { +func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint) ([]edgeJobResponse, *httperror.HandlerError) { schedules := []edgeJobResponse{} edgeJobs, err := tx.EdgeJob().ReadAll() @@ -216,11 +216,16 @@ func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID p return nil, httperror.InternalServerError("Unable to retrieve Edge Jobs", err) } + endpointGroups, err := tx.EndpointGroup().ReadAll() + if err != nil { + return nil, httperror.InternalServerError("Unable to retrieve endpoint groups", err) + } + for _, job := range edgeJobs { - _, endpointHasJob := job.Endpoints[endpointID] + _, endpointHasJob := job.Endpoints[endpoint.ID] if !endpointHasJob { for _, edgeGroupID := range job.EdgeGroups { - member, _, err := edge.EndpointInEdgeGroup(tx, endpointID, edgeGroupID) + member, _, err := edge.EndpointInEdgeGroup(tx, endpoint, edgeGroupID, endpointGroups) if err != nil { return nil, httperror.InternalServerError("Unable to retrieve relations", err) } else if member { @@ -236,10 +241,10 @@ func (handler *Handler) buildSchedules(tx dataservices.DataStoreTx, endpointID p } var collectLogs bool - if _, ok := job.GroupLogsCollection[endpointID]; ok { - collectLogs = job.GroupLogsCollection[endpointID].CollectLogs + if _, ok := job.GroupLogsCollection[endpoint.ID]; ok { + collectLogs = job.GroupLogsCollection[endpoint.ID].CollectLogs } else { - collectLogs = job.Endpoints[endpointID].CollectLogs + collectLogs = job.Endpoints[endpoint.ID].CollectLogs } schedule := edgeJobResponse{ diff --git a/api/internal/edge/edgegroup.go b/api/internal/edge/edgegroup.go index eae4fedce..b9a1b899d 100644 --- a/api/internal/edge/edgegroup.go +++ b/api/internal/edge/edgegroup.go @@ -4,13 +4,22 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/internal/endpointutils" + "github.com/portainer/portainer/api/roar" "github.com/portainer/portainer/api/tag" ) // EdgeGroupRelatedEndpoints returns a list of environments(endpoints) related to this Edge group func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup) []portainer.EndpointID { if !edgeGroup.Dynamic { - return edgeGroup.EndpointIDs.ToSlice() + var r roar.Roar[portainer.EndpointID] + + for _, endpoint := range endpoints { + if edgeGroup.EndpointIDs.Contains(endpoint.ID) { + r.Add(endpoint.ID) + } + } + + return r.ToSlice() } endpointGroupsMap := map[portainer.EndpointGroupID]*portainer.EndpointGroup{} diff --git a/api/internal/edge/endpoint.go b/api/internal/edge/endpoint.go index 27cc50b3b..7901b0e88 100644 --- a/api/internal/edge/endpoint.go +++ b/api/internal/edge/endpoint.go @@ -1,12 +1,9 @@ package edge import ( - "slices" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" - - "github.com/rs/zerolog/log" + "github.com/portainer/portainer/api/internal/endpointutils" ) // EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Environment(Endpoint) @@ -47,27 +44,22 @@ func EffectiveCheckinInterval(tx dataservices.DataStoreTx, endpoint *portainer.E // EndpointInEdgeGroup returns true and the edge group name if the endpoint is in the edge group func EndpointInEdgeGroup( tx dataservices.DataStoreTx, - endpointID portainer.EndpointID, + endpoint *portainer.Endpoint, edgeGroupID portainer.EdgeGroupID, + endpointGroups []portainer.EndpointGroup, ) (bool, string, error) { - endpointIDs, err := GetEndpointsFromEdgeGroups( - []portainer.EdgeGroupID{edgeGroupID}, tx, - ) + if !endpointutils.IsEdgeEndpoint(endpoint) || !endpoint.UserTrusted { + return false, "", nil + } + + edgeGroup, err := tx.EdgeGroup().Read(edgeGroupID) if err != nil { return false, "", err } - if slices.Contains(endpointIDs, endpointID) { - edgeGroup, err := tx.EdgeGroup().Read(edgeGroupID) - if err != nil { - log.Warn(). - Err(err). - Int("edgeGroupID", int(edgeGroupID)). - Msg("Unable to retrieve edge group") - - return false, "", err - } + r := EdgeGroupRelatedEndpoints(edgeGroup, []portainer.Endpoint{*endpoint}, endpointGroups) + if len(r) > 0 { return true, edgeGroup.Name, nil } diff --git a/api/internal/edge/endpoint_test.go b/api/internal/edge/endpoint_test.go new file mode 100644 index 000000000..241408515 --- /dev/null +++ b/api/internal/edge/endpoint_test.go @@ -0,0 +1,82 @@ +package edge + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/datastore" + "github.com/portainer/portainer/api/roar" + + "github.com/stretchr/testify/require" +) + +func TestEndpointInEdgeGroup(t *testing.T) { + _, store := datastore.MustNewTestStore(t, true, false) + + endpointGroups := []portainer.EndpointGroup{{ID: 1, Name: "test-group"}} + + endpoint := &portainer.Endpoint{ + ID: 1, + Name: "test-endpoint", + Type: portainer.EdgeAgentOnDockerEnvironment, + UserTrusted: true, + GroupID: endpointGroups[0].ID, + } + edgeGroupID := portainer.EdgeGroupID(1) + + untrustedEndpoint := &portainer.Endpoint{ + ID: 2, + Name: "untrusted-endpoint", + Type: portainer.EdgeAgentOnDockerEnvironment, + UserTrusted: false, + GroupID: endpointGroups[0].ID, + } + + nonEdgeEndpoint := &portainer.Endpoint{ + ID: 2, + Name: "untrusted-endpoint", + Type: portainer.AgentOnDockerEnvironment, + UserTrusted: true, + GroupID: endpointGroups[0].ID, + } + + err := store.EdgeGroup().Create(&portainer.EdgeGroup{ + ID: edgeGroupID, + Name: "test-edge-group", + Dynamic: false, + EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpoint.ID, untrustedEndpoint.ID}), + }) + require.NoError(t, err) + + // Related endpoint in a static edge group + + inEdgeGroup, _, err := EndpointInEdgeGroup(store, endpoint, edgeGroupID, endpointGroups) + require.NoError(t, err) + require.True(t, inEdgeGroup) + + // Unrelated endpoint in a static edge group + + unrelatedEndpoint := &portainer.Endpoint{ + ID: 3, + Name: "unrelated-endpoint", + Type: portainer.EdgeAgentOnDockerEnvironment, + UserTrusted: true, + GroupID: 0, + } + + inEdgeGroup, _, err = EndpointInEdgeGroup(store, unrelatedEndpoint, edgeGroupID, endpointGroups) + require.NoError(t, err) + require.False(t, inEdgeGroup) + + // Untrusted endpoint + + inEdgeGroup, _, err = EndpointInEdgeGroup(store, untrustedEndpoint, edgeGroupID, endpointGroups) + require.NoError(t, err) + require.False(t, inEdgeGroup) + + // Non-edge endpoint + + inEdgeGroup, _, err = EndpointInEdgeGroup(store, nonEdgeEndpoint, edgeGroupID, endpointGroups) + require.NoError(t, err) + require.False(t, inEdgeGroup) +}