diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index 6c7f6de94..8ef988345 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -256,7 +256,7 @@ func (handler *Handler) filterEndpointsByQuery( return filteredEndpoints, totalAvailableEndpoints, nil } -func endpointStatusInStackMatchesFilter(stackStatus *portainer.EdgeStackStatusForEnv, envId portainer.EndpointID, statusFilter portainer.EdgeStackStatusType) bool { +func endpointStatusInStackMatchesFilter(stackStatus *portainer.EdgeStackStatusForEnv, statusFilter portainer.EdgeStackStatusType) bool { // consider that if the env has no status in the stack it is in Pending state if statusFilter == portainer.EdgeStackStatusPending { return stackStatus == nil || len(stackStatus.Status) == 0 @@ -272,55 +272,62 @@ func endpointStatusInStackMatchesFilter(stackStatus *portainer.EdgeStackStatusFo } func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId portainer.EdgeStackID, statusFilter *portainer.EdgeStackStatusType, datastore dataservices.DataStore) ([]portainer.Endpoint, error) { - stack, err := datastore.EdgeStack().EdgeStack(edgeStackId) - if err != nil { - return nil, errors.WithMessage(err, "Unable to retrieve edge stack from the database") - } - - envIds := roar.Roar[portainer.EndpointID]{} - for _, edgeGroupdId := range stack.EdgeGroups { - edgeGroup, err := datastore.EdgeGroup().Read(edgeGroupdId) + var filteredEndpoints []portainer.Endpoint + if err := datastore.ViewTx(func(tx dataservices.DataStoreTx) error { + stack, err := tx.EdgeStack().EdgeStack(edgeStackId) if err != nil { - return nil, errors.WithMessage(err, "Unable to retrieve edge group from the database") + return errors.WithMessage(err, "Unable to retrieve edge stack from the database") } - if edgeGroup.Dynamic { - endpointIDs, err := edgegroups.GetEndpointsByTags(datastore, edgeGroup.TagIDs, edgeGroup.PartialMatch) + envIds := roar.Roar[portainer.EndpointID]{} + for _, edgeGroupId := range stack.EdgeGroups { + edgeGroup, err := tx.EdgeGroup().Read(edgeGroupId) if err != nil { - return nil, errors.WithMessage(err, "Unable to retrieve environments and environment groups for Edge group") + return errors.WithMessage(err, "Unable to retrieve edge group from the database") } - edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs) + + if edgeGroup.Dynamic { + endpointIDs, err := edgegroups.GetEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch) + if err != nil { + return errors.WithMessage(err, "Unable to retrieve environments and environment groups for Edge group") + } + edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs) + } + + envIds.Union(edgeGroup.EndpointIDs) } - envIds.Union(edgeGroup.EndpointIDs) - } + filteredEnvIds := roar.Roar[portainer.EndpointID]{} + filteredEnvIds.Union(envIds) - if statusFilter != nil { - var innerErr error + if statusFilter != nil { + var innerErr error + + envIds.Iterate(func(envId portainer.EndpointID) bool { + edgeStackStatus, err := tx.EdgeStackStatus().Read(edgeStackId, envId) + if err != nil && !dataservices.IsErrObjectNotFound(err) { + innerErr = errors.WithMessagef(err, "Unable to retrieve edge stack status for environment %d", envId) + return false + } + + if !endpointStatusInStackMatchesFilter(edgeStackStatus, *statusFilter) { + filteredEnvIds.Remove(envId) + } - envIds.Iterate(func(envId portainer.EndpointID) bool { - edgeStackStatus, err := datastore.EdgeStackStatus().Read(edgeStackId, envId) - if dataservices.IsErrObjectNotFound(err) { return true - } else if err != nil { - innerErr = errors.WithMessagef(err, "Unable to retrieve edge stack status for environment %d", envId) - return false + }) + + if innerErr != nil { + return innerErr } - - if !endpointStatusInStackMatchesFilter(edgeStackStatus, portainer.EndpointID(envId), *statusFilter) { - envIds.Remove(envId) - } - - return true - }) - - if innerErr != nil { - return nil, innerErr } + + filteredEndpoints = filteredEndpointsByIds(endpoints, filteredEnvIds) + + return nil + }); err != nil { + return nil, err } - - filteredEndpoints := filteredEndpointsByIds(endpoints, envIds) - return filteredEndpoints, nil } diff --git a/api/http/handler/endpoints/filter_test.go b/api/http/handler/endpoints/filter_test.go index 794bf413e..aab26f5af 100644 --- a/api/http/handler/endpoints/filter_test.go +++ b/api/http/handler/endpoints/filter_test.go @@ -5,6 +5,7 @@ import ( "testing" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/testhelpers" @@ -304,42 +305,103 @@ func TestFilterEndpointsByEdgeStack(t *testing.T) { _, store := datastore.MustNewTestStore(t, false, false) endpoints := []portainer.Endpoint{ - {ID: 1, Name: "Endpoint 1"}, - {ID: 2, Name: "Endpoint 2"}, - {ID: 3, Name: "Endpoint 3"}, + {ID: 1, Name: "Endpoint 1", Type: portainer.EdgeAgentOnDockerEnvironment, UserTrusted: true}, + {ID: 2, Name: "Endpoint 2", TagIDs: []portainer.TagID{1}, Type: portainer.EdgeAgentOnDockerEnvironment, UserTrusted: true}, + {ID: 3, Name: "Endpoint 3", TagIDs: []portainer.TagID{1}, Type: portainer.EdgeAgentOnDockerEnvironment, UserTrusted: true}, {ID: 4, Name: "Endpoint 4"}, } edgeStackId := portainer.EdgeStackID(1) + require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { + require.NoError(t, tx.Tag().Create(&portainer.Tag{ID: 1, Name: "tag", Endpoints: map[portainer.EndpointID]bool{2: true, 3: true}})) - err := store.EdgeStack().Create(edgeStackId, &portainer.EdgeStack{ - ID: edgeStackId, - Name: "Test Edge Stack", - EdgeGroups: []portainer.EdgeGroupID{1, 2}, + for i := range endpoints { + require.NoError(t, tx.Endpoint().Create(&endpoints[i])) + } + + require.NoError(t, tx.EdgeStack().Create(edgeStackId, &portainer.EdgeStack{ + ID: edgeStackId, + Name: "Test Edge Stack", + EdgeGroups: []portainer.EdgeGroupID{1, 2}, + })) + + require.NoError(t, tx.EdgeGroup().Create(&portainer.EdgeGroup{ + ID: 1, + Name: "Edge Group 1", + EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}), + })) + + require.NoError(t, tx.EdgeGroup().Create(&portainer.EdgeGroup{ + ID: 2, + Name: "Edge Group 2", + Dynamic: true, + TagIDs: []portainer.TagID{1}, + })) + + require.NoError(t, tx.EdgeStackStatus().Create(edgeStackId, endpoints[0].ID, &portainer.EdgeStackStatusForEnv{ + Status: []portainer.EdgeStackDeploymentStatus{{Type: portainer.EdgeStackStatusAcknowledged}}})) + + return nil + })) + + test := func(status *portainer.EdgeStackStatusType, expected []portainer.Endpoint) { + tmp := make([]portainer.Endpoint, len(endpoints)) + require.Equal(t, 4, copy(tmp, endpoints)) + es, err := filterEndpointsByEdgeStack(tmp, edgeStackId, status, store) + require.NoError(t, err) + // validate that the len is the same + require.Len(t, es, len(expected)) + // and that all items are the expected ones + for i := range expected { + require.Contains(t, es, expected[i]) + } + } + + test(nil, []portainer.Endpoint{endpoints[0], endpoints[1], endpoints[2]}) + + status := portainer.EdgeStackStatusPending + test(&status, []portainer.Endpoint{endpoints[1], endpoints[2]}) + + status = portainer.EdgeStackStatusCompleted + test(&status, []portainer.Endpoint{}) + + status = portainer.EdgeStackStatusAcknowledged + test(&status, []portainer.Endpoint{endpoints[0]}) // that's the only one with an edge stack status in DB +} + +func TestErrorsFilterEndpointsByEdgeStack(t *testing.T) { + t.Run("must error by edge stack not found", func(t *testing.T) { + _, store := datastore.MustNewTestStore(t, false, false) + require.NotNil(t, store) + + _, err := filterEndpointsByEdgeStack([]portainer.Endpoint{}, 1, nil, store) + require.Error(t, err) }) - require.NoError(t, err) - err = store.EdgeGroup().Create(&portainer.EdgeGroup{ - ID: 1, - Name: "Edge Group 1", - EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}), + t.Run("must error by edge group not found", func(t *testing.T) { + _, store := datastore.MustNewTestStore(t, false, false) + require.NotNil(t, store) + + require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { + require.NoError(t, tx.EdgeStack().Create(1, &portainer.EdgeStack{ID: 1, Name: "1", EdgeGroups: []portainer.EdgeGroupID{1}})) + return nil + })) + _, err := filterEndpointsByEdgeStack([]portainer.Endpoint{}, 1, nil, store) + require.Error(t, err) }) - require.NoError(t, err) - err = store.EdgeGroup().Create(&portainer.EdgeGroup{ - ID: 2, - Name: "Edge Group 2", - EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}), + t.Run("must error by env tag not found", func(t *testing.T) { + _, store := datastore.MustNewTestStore(t, false, false) + require.NotNil(t, store) + + require.NoError(t, store.UpdateTx(func(tx dataservices.DataStoreTx) error { + require.NoError(t, tx.EdgeStack().Create(1, &portainer.EdgeStack{ID: 1, Name: "1", EdgeGroups: []portainer.EdgeGroupID{1}})) + require.NoError(t, tx.EdgeGroup().Create(&portainer.EdgeGroup{ID: 1, Name: "edge group", Dynamic: true, TagIDs: []portainer.TagID{1}})) + return nil + })) + _, err := filterEndpointsByEdgeStack([]portainer.Endpoint{}, 1, nil, store) + require.Error(t, err) }) - require.NoError(t, err) - - es, err := filterEndpointsByEdgeStack(endpoints, edgeStackId, nil, store) - require.NoError(t, err) - require.Len(t, es, 3) - require.Contains(t, es, endpoints[0]) // Endpoint 1 - require.Contains(t, es, endpoints[1]) // Endpoint 2 - require.Contains(t, es, endpoints[2]) // Endpoint 3 - require.NotContains(t, es, endpoints[3]) // Endpoint 4 } func TestFilterEndpointsByEdgeGroup(t *testing.T) {