diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index f691ae15c..f632112b5 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -125,8 +125,8 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * if payload.GroupID != nil { groupID := portainer.EndpointGroupID(*payload.GroupID) - endpoint.GroupID = groupID updateRelations = updateRelations || groupID != endpoint.GroupID + endpoint.GroupID = groupID } if payload.TagIDs != nil { diff --git a/api/internal/edge/edgegroup.go b/api/internal/edge/edgegroup.go index fb8d38ed0..edbe6eaa9 100644 --- a/api/internal/edge/edgegroup.go +++ b/api/internal/edge/edgegroup.go @@ -69,7 +69,7 @@ func GetEndpointsFromEdgeGroups(edgeGroupIDs []portainer.EdgeGroupID, datastore return response, nil } -// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with environment(endpoint) +// edgeGroupRelatedToEndpoint returns true if edgeGroup is associated with environment(endpoint) func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) bool { if !edgeGroup.Dynamic { for _, endpointID := range edgeGroup.Endpoints { @@ -91,5 +91,5 @@ func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portai return len(intersection) != 0 } - return tag.Contains(edgeGroupTags, endpointTags) + return tag.FullMatch(edgeGroupTags, endpointTags) } diff --git a/api/internal/tag/tag.go b/api/internal/tag/tag.go index ada130c88..89f52c3aa 100644 --- a/api/internal/tag/tag.go +++ b/api/internal/tag/tag.go @@ -50,13 +50,16 @@ func Union(sets ...tagSet) tagSet { // Contains return true if setA contains setB func Contains(setA tagSet, setB tagSet) bool { - containedTags := 0 + if len(setA) == 0 || len(setB) == 0 { + return false + } + for tag := range setB { - if setA[tag] { - containedTags++ + if !setA[tag] { + return false } } - return containedTags == len(setA) + return true } // Difference returns the set difference tagsA - tagsB diff --git a/api/internal/tag/tag_match.go b/api/internal/tag/tag_match.go new file mode 100644 index 000000000..fe6226ea0 --- /dev/null +++ b/api/internal/tag/tag_match.go @@ -0,0 +1,11 @@ +package tag + +// FullMatch returns true if environment tags matches all edge group tags +func FullMatch(edgeGroupTags tagSet, environmentTags tagSet) bool { + return Contains(environmentTags, edgeGroupTags) +} + +// PartialMatch returns true if environment tags matches at least one edge group tag +func PartialMatch(edgeGroupTags tagSet, environmentTags tagSet) bool { + return len(Intersection(edgeGroupTags, environmentTags)) != 0 +} diff --git a/api/internal/tag/tag_match_test.go b/api/internal/tag/tag_match_test.go new file mode 100644 index 000000000..6a295eb8e --- /dev/null +++ b/api/internal/tag/tag_match_test.go @@ -0,0 +1,135 @@ +package tag + +import ( + "testing" + + portainer "github.com/portainer/portainer/api" +) + +func TestFullMatch(t *testing.T) { + cases := []struct { + name string + edgeGroupTags tagSet + environmentTag tagSet + expected bool + }{ + { + name: "environment tag partially match edge group tags", + edgeGroupTags: Set([]portainer.TagID{1, 2, 3}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: false, + }, + { + name: "edge group tags equal to environment tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: true, + }, + { + name: "environment tags fully match edge group tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{1, 2, 3}), + expected: true, + }, + { + name: "environment tags do not match edge group tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{3, 4}), + expected: false, + }, + { + name: "edge group has no tags and environment has tags", + edgeGroupTags: Set([]portainer.TagID{}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: false, + }, + { + name: "edge group has tags and environment has no tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{}), + expected: false, + }, + { + name: "both edge group and environment have no tags", + edgeGroupTags: Set([]portainer.TagID{}), + environmentTag: Set([]portainer.TagID{}), + expected: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := FullMatch(tc.edgeGroupTags, tc.environmentTag) + if result != tc.expected { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestPartialMatch(t *testing.T) { + cases := []struct { + name string + edgeGroupTags tagSet + environmentTag tagSet + expected bool + }{ + { + name: "environment tags partially match edge group tags 1", + edgeGroupTags: Set([]portainer.TagID{1, 2, 3}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: true, + }, + { + name: "environment tags partially match edge group tags 2", + edgeGroupTags: Set([]portainer.TagID{1, 2, 3}), + environmentTag: Set([]portainer.TagID{1, 4, 5}), + expected: true, + }, + { + name: "edge group tags equal to environment tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: true, + }, + { + name: "environment tags fully match edge group tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{1, 2, 3}), + expected: true, + }, + { + name: "environment tags do not match edge group tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{3, 4}), + expected: false, + }, + { + name: "edge group has no tags and environment has tags", + edgeGroupTags: Set([]portainer.TagID{}), + environmentTag: Set([]portainer.TagID{1, 2}), + expected: false, + }, + { + name: "edge group has tags and environment has no tags", + edgeGroupTags: Set([]portainer.TagID{1, 2}), + environmentTag: Set([]portainer.TagID{}), + expected: false, + }, + { + name: "both edge group and environment have no tags", + edgeGroupTags: Set([]portainer.TagID{}), + environmentTag: Set([]portainer.TagID{}), + expected: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := PartialMatch(tc.edgeGroupTags, tc.environmentTag) + if result != tc.expected { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} diff --git a/api/internal/tag/tag_test.go b/api/internal/tag/tag_test.go new file mode 100644 index 000000000..168adbef8 --- /dev/null +++ b/api/internal/tag/tag_test.go @@ -0,0 +1,204 @@ +package tag + +import ( + "reflect" + "testing" + + portainer "github.com/portainer/portainer/api" +) + +func TestIntersection(t *testing.T) { + cases := []struct { + name string + setA tagSet + setB tagSet + expected tagSet + }{ + { + name: "positive numbers set intersection", + setA: Set([]portainer.TagID{1, 2, 3, 4, 5}), + setB: Set([]portainer.TagID{4, 5, 6, 7}), + expected: Set([]portainer.TagID{4, 5}), + }, + { + name: "empty setA intersection", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{}), + expected: Set([]portainer.TagID{}), + }, + { + name: "empty setB intersection", + setA: Set([]portainer.TagID{}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: Set([]portainer.TagID{}), + }, + { + name: "no common elements sets intersection", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{4, 5, 6}), + expected: Set([]portainer.TagID{}), + }, + { + name: "equal sets intersection", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := Intersection(tc.setA, tc.setB) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestUnion(t *testing.T) { + cases := []struct { + name string + setA tagSet + setB tagSet + expected tagSet + }{ + { + name: "non-duplicate set union", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{4, 5, 6}), + expected: Set([]portainer.TagID{1, 2, 3, 4, 5, 6}), + }, + { + name: "empty setA union", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + { + name: "empty setB union", + setA: Set([]portainer.TagID{}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + { + name: "duplicate elements in set union", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{3, 4, 5}), + expected: Set([]portainer.TagID{1, 2, 3, 4, 5}), + }, + { + name: "equal sets union", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := Union(tc.setA, tc.setB) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestContains(t *testing.T) { + cases := []struct { + name string + setA tagSet + setB tagSet + expected bool + }{ + { + name: "setA contains setB", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{1, 2}), + expected: true, + }, + { + name: "setA equals to setB", + setA: Set([]portainer.TagID{1, 2}), + setB: Set([]portainer.TagID{1, 2}), + expected: true, + }, + { + name: "setA contains parts of setB", + setA: Set([]portainer.TagID{1, 2}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: false, + }, + { + name: "setA does not contain setB", + setA: Set([]portainer.TagID{1, 2}), + setB: Set([]portainer.TagID{3, 4}), + expected: false, + }, + { + name: "setA is empty and setB is not empty", + setA: Set([]portainer.TagID{}), + setB: Set([]portainer.TagID{1, 2}), + expected: false, + }, + { + name: "setA is not empty and setB is empty", + setA: Set([]portainer.TagID{1, 2}), + setB: Set([]portainer.TagID{}), + expected: false, + }, + { + name: "setA is empty and setB is empty", + setA: Set([]portainer.TagID{}), + setB: Set([]portainer.TagID{}), + expected: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := Contains(tc.setA, tc.setB) + if result != tc.expected { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +} + +func TestDifference(t *testing.T) { + cases := []struct { + name string + setA tagSet + setB tagSet + expected tagSet + }{ + { + name: "positive numbers set difference", + setA: Set([]portainer.TagID{1, 2, 3, 4, 5}), + setB: Set([]portainer.TagID{4, 5, 6, 7}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + { + name: "empty set difference", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{}), + expected: Set([]portainer.TagID{1, 2, 3}), + }, + { + name: "equal sets difference", + setA: Set([]portainer.TagID{1, 2, 3}), + setB: Set([]portainer.TagID{1, 2, 3}), + expected: Set([]portainer.TagID{}), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := Difference(tc.setA, tc.setB) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %v, got %v", tc.expected, result) + } + }) + } +}