mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(waiting-room): choose relations when associated endpoint [EE-5187] (#8720)
This commit is contained in:
parent
511adabce2
commit
365316971b
53 changed files with 1712 additions and 303 deletions
|
@ -39,7 +39,7 @@ func Test_EndpointList_AgentVersion(t *testing.T) {
|
|||
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
|
||||
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
|
||||
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
handler, teardown := setupEndpointListHandler(t, []portainer.Endpoint{
|
||||
notAgentEnvironments,
|
||||
version1Endpoint,
|
||||
version2Endpoint,
|
||||
|
@ -111,7 +111,7 @@ func Test_endpointList_edgeFilter(t *testing.T) {
|
|||
regularTrustedEdgeStandard := portainer.Endpoint{ID: 4, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
handler, teardown := setupEndpointListHandler(t, []portainer.Endpoint{
|
||||
trustedEdgeAsync,
|
||||
untrustedEdgeAsync,
|
||||
regularUntrustedEdgeStandard,
|
||||
|
@ -184,7 +184,7 @@ func Test_endpointList_edgeFilter(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
func setupEndpointListHandler(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
|
|
|
@ -9,9 +9,8 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/tag"
|
||||
)
|
||||
|
||||
type endpointUpdatePayload struct {
|
||||
|
@ -120,48 +119,31 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
||||
}
|
||||
|
||||
groupIDChanged := false
|
||||
updateRelations := false
|
||||
|
||||
if payload.GroupID != nil {
|
||||
groupID := portainer.EndpointGroupID(*payload.GroupID)
|
||||
groupIDChanged = groupID != endpoint.GroupID
|
||||
|
||||
endpoint.GroupID = groupID
|
||||
updateRelations = updateRelations || groupID != endpoint.GroupID
|
||||
}
|
||||
|
||||
tagsChanged := false
|
||||
if payload.TagIDs != nil {
|
||||
payloadTagSet := tag.Set(payload.TagIDs)
|
||||
endpointTagSet := tag.Set((endpoint.TagIDs))
|
||||
union := tag.Union(payloadTagSet, endpointTagSet)
|
||||
intersection := tag.Intersection(payloadTagSet, endpointTagSet)
|
||||
tagsChanged = len(union) > len(intersection)
|
||||
err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
|
||||
if tagsChanged {
|
||||
removeTags := tag.Difference(endpointTagSet, payloadTagSet)
|
||||
|
||||
for tagID := range removeTags {
|
||||
err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) {
|
||||
delete(tag.Endpoints, endpoint.ID)
|
||||
})
|
||||
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
tagsChanged, err := updateEnvironmentTags(tx, payload.TagIDs, endpoint.TagIDs, endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.TagIDs = payload.TagIDs
|
||||
for _, tagID := range payload.TagIDs {
|
||||
err = handler.DataStore.Tag().UpdateTagFunc(tagID, func(tag *portainer.Tag) {
|
||||
tag.Endpoints[endpoint.ID] = true
|
||||
})
|
||||
updateRelations = updateRelations || tagsChanged
|
||||
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
httperror.InternalServerError("Unable to update environment tags", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,39 +268,13 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
return httperror.InternalServerError("Unable to persist environment changes inside the database", err)
|
||||
}
|
||||
|
||||
if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if updateRelations {
|
||||
err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.updateEdgeRelations(tx, endpoint)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to find environment relation inside the database", err)
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to find environment group inside the database", err)
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge groups from the database", err)
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve edge stacks from the database", err)
|
||||
}
|
||||
|
||||
currentEdgeStackSet := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
endpointEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
for _, edgeStackID := range endpointEdgeStacks {
|
||||
currentEdgeStackSet[edgeStackID] = true
|
||||
}
|
||||
|
||||
relation.EdgeStacks = currentEdgeStackSet
|
||||
|
||||
err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment relation changes inside the database", err)
|
||||
return httperror.InternalServerError("Unable to update environment relations", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
110
api/http/handler/endpoints/endpoint_update_relations.go
Normal file
110
api/http/handler/endpoints/endpoint_update_relations.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
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/dataservices"
|
||||
)
|
||||
|
||||
type endpointUpdateRelationsPayload struct {
|
||||
Relations map[portainer.EndpointID]struct {
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
Tags []portainer.TagID
|
||||
Group portainer.EndpointGroupID
|
||||
}
|
||||
}
|
||||
|
||||
func (payload *endpointUpdateRelationsPayload) Validate(r *http.Request) error {
|
||||
for eID := range payload.Relations {
|
||||
if eID == 0 {
|
||||
return errors.New("Missing environment identifier")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EndpointUpdateRelations
|
||||
// @summary Update relations for a list of environments
|
||||
// @description Update relations for a list of environments
|
||||
// @description Edge groups, tags and environment group can be updated.
|
||||
// @description
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoints
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @param body body endpointUpdateRelationsPayload true "Environment relations data"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 401 "Unauthorized"
|
||||
// @failure 404 "Not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints/relations [put]
|
||||
func (handler *Handler) updateRelations(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
payload, err := request.GetPayload[endpointUpdateRelationsPayload](r)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
for environmentID, relationPayload := range payload.Relations {
|
||||
endpoint, err := tx.Endpoint().Endpoint(environmentID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find an environment with the specified identifier inside the database")
|
||||
}
|
||||
|
||||
updateRelations := false
|
||||
|
||||
if relationPayload.Group != 0 {
|
||||
groupIDChanged := endpoint.GroupID != relationPayload.Group
|
||||
endpoint.GroupID = relationPayload.Group
|
||||
updateRelations = updateRelations || groupIDChanged
|
||||
}
|
||||
|
||||
if relationPayload.Tags != nil {
|
||||
tagsChanged, err := updateEnvironmentTags(tx, relationPayload.Tags, endpoint.TagIDs, endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment tags")
|
||||
}
|
||||
|
||||
endpoint.TagIDs = relationPayload.Tags
|
||||
updateRelations = updateRelations || tagsChanged
|
||||
}
|
||||
|
||||
if relationPayload.EdgeGroups != nil {
|
||||
edgeGroupsChanged, err := updateEnvironmentEdgeGroups(tx, relationPayload.EdgeGroups, endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment edge groups")
|
||||
}
|
||||
|
||||
updateRelations = updateRelations || edgeGroupsChanged
|
||||
}
|
||||
|
||||
if updateRelations {
|
||||
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment")
|
||||
}
|
||||
|
||||
err = handler.updateEdgeRelations(tx, endpoint)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to update environment relations")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment relations", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -69,6 +69,7 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
|||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/agent_versions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/relations", bouncer.RestrictedAccess(httperror.LoggerHandler(h.updateRelations))).Methods(http.MethodPut)
|
||||
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
|
|
48
api/http/handler/endpoints/update_edge_relations.go
Normal file
48
api/http/handler/endpoints/update_edge_relations.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
)
|
||||
|
||||
// updateEdgeRelations updates the edge stacks associated to an edge endpoint
|
||||
func (handler *Handler) updateEdgeRelations(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint) error {
|
||||
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||
return nil
|
||||
}
|
||||
|
||||
relation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find environment relation inside the database")
|
||||
}
|
||||
|
||||
endpointGroup, err := tx.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find environment group inside the database")
|
||||
}
|
||||
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to retrieve edge groups from the database")
|
||||
}
|
||||
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to retrieve edge stacks from the database")
|
||||
}
|
||||
|
||||
currentEdgeStackSet := set.ToSet(edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks))
|
||||
|
||||
relation.EdgeStacks = currentEdgeStackSet
|
||||
|
||||
err = tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to persist environment relation changes inside the database")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
72
api/http/handler/endpoints/utils_update_edge_groups.go
Normal file
72
api/http/handler/endpoints/utils_update_edge_groups.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
"github.com/portainer/portainer/api/internal/slices"
|
||||
)
|
||||
|
||||
func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []portainer.EdgeGroupID, environmentID portainer.EndpointID) (bool, error) {
|
||||
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
|
||||
if err != nil {
|
||||
return false, errors.WithMessage(err, "Unable to retrieve edge groups from the database")
|
||||
}
|
||||
|
||||
newEdgeGroupsSet := set.ToSet(newEdgeGroups)
|
||||
|
||||
environmentEdgeGroupsSet := set.Set[portainer.EdgeGroupID]{}
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
for _, eID := range edgeGroup.Endpoints {
|
||||
if eID == environmentID {
|
||||
environmentEdgeGroupsSet[edgeGroup.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
union := set.Union(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
||||
intersection := set.Intersection(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
||||
|
||||
if len(union) <= len(intersection) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
updateSet := func(groupIDs set.Set[portainer.EdgeGroupID], updateItem func(*portainer.EdgeGroup)) error {
|
||||
for groupID := range groupIDs {
|
||||
group, err := tx.EdgeGroup().EdgeGroup(groupID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find a Edge group inside the database")
|
||||
}
|
||||
|
||||
updateItem(group)
|
||||
|
||||
err = tx.EdgeGroup().UpdateEdgeGroup(groupID, group)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to persist Edge group changes inside the database")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
removeEdgeGroups := environmentEdgeGroupsSet.Difference(newEdgeGroupsSet)
|
||||
err = updateSet(removeEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
||||
edgeGroup.Endpoints = slices.RemoveItem(edgeGroup.Endpoints, func(eID portainer.EndpointID) bool {
|
||||
return eID == environmentID
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
addToEdgeGroups := newEdgeGroupsSet.Difference(environmentEdgeGroupsSet)
|
||||
err = updateSet(addToEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
||||
edgeGroup.Endpoints = append(edgeGroup.Endpoints, environmentID)
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
156
api/http/handler/endpoints/utils_update_edge_groups_test.go
Normal file
156
api/http/handler/endpoints/utils_update_edge_groups_test.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_updateEdgeGroups(t *testing.T) {
|
||||
|
||||
createGroups := func(store *datastore.Store, names []string) ([]portainer.EdgeGroup, error) {
|
||||
groups := make([]portainer.EdgeGroup, len(names))
|
||||
for index, name := range names {
|
||||
group := &portainer.EdgeGroup{
|
||||
Name: name,
|
||||
Dynamic: false,
|
||||
TagIDs: make([]portainer.TagID, 0),
|
||||
Endpoints: make([]portainer.EndpointID, 0),
|
||||
}
|
||||
|
||||
err := store.EdgeGroup().Create(group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups[index] = *group
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
checkGroups := func(store *datastore.Store, is *assert.Assertions, groupIDs []portainer.EdgeGroupID, endpointID portainer.EndpointID) {
|
||||
for _, groupID := range groupIDs {
|
||||
group, err := store.EdgeGroup().EdgeGroup(groupID)
|
||||
is.NoError(err)
|
||||
|
||||
for _, endpoint := range group.Endpoints {
|
||||
if endpoint == endpointID {
|
||||
return
|
||||
}
|
||||
}
|
||||
is.Fail("expected endpoint to be in group")
|
||||
}
|
||||
}
|
||||
|
||||
groupsByName := func(groups []portainer.EdgeGroup, groupNames []string) []portainer.EdgeGroup {
|
||||
result := make([]portainer.EdgeGroup, len(groupNames))
|
||||
for i, tagName := range groupNames {
|
||||
for j, tag := range groups {
|
||||
if tag.Name == tagName {
|
||||
result[i] = groups[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
title string
|
||||
endpoint *portainer.Endpoint
|
||||
groupNames []string
|
||||
endpointGroupNames []string
|
||||
groupsToApply []string
|
||||
shouldNotBeUpdated bool
|
||||
}
|
||||
|
||||
testFn := func(t *testing.T, testCase testCase) {
|
||||
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().Create(testCase.endpoint)
|
||||
is.NoError(err)
|
||||
|
||||
groups, err := createGroups(store, testCase.groupNames)
|
||||
is.NoError(err)
|
||||
|
||||
endpointGroups := groupsByName(groups, testCase.endpointGroupNames)
|
||||
for _, group := range endpointGroups {
|
||||
group.Endpoints = append(group.Endpoints, testCase.endpoint.ID)
|
||||
|
||||
err = store.EdgeGroup().UpdateEdgeGroup(group.ID, &group)
|
||||
is.NoError(err)
|
||||
}
|
||||
|
||||
expectedGroups := groupsByName(groups, testCase.groupsToApply)
|
||||
expectedIDs := make([]portainer.EdgeGroupID, len(expectedGroups))
|
||||
for i, tag := range expectedGroups {
|
||||
expectedIDs[i] = tag.ID
|
||||
}
|
||||
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
updated, err := updateEnvironmentEdgeGroups(tx, expectedIDs, testCase.endpoint.ID)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(testCase.shouldNotBeUpdated, !updated)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
checkGroups(store, is, expectedIDs, testCase.endpoint.ID)
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
title: "applying edge groups to an endpoint without edge groups",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
groupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
endpointGroupNames: []string{},
|
||||
groupsToApply: []string{"edge group1", "edge group2", "edge group3"},
|
||||
},
|
||||
{
|
||||
title: "applying edge groups to an endpoint with edge groups",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
groupNames: []string{"edge group1", "edge group2", "edge group3", "edge group4", "edge group5", "edge group6"},
|
||||
endpointGroupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
groupsToApply: []string{"edge group4", "edge group5", "edge group6"},
|
||||
},
|
||||
{
|
||||
title: "applying edge groups to an endpoint with edge groups that are already applied",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
groupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
endpointGroupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
groupsToApply: []string{"edge group1", "edge group2", "edge group3"},
|
||||
shouldNotBeUpdated: true,
|
||||
},
|
||||
{
|
||||
title: "adding new edge groups to an endpoint with edge groups ",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
groupNames: []string{"edge group1", "edge group2", "edge group3", "edge group4", "edge group5", "edge group6"},
|
||||
endpointGroupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
groupsToApply: []string{"edge group1", "edge group2", "edge group3", "edge group4", "edge group5", "edge group6"},
|
||||
},
|
||||
{
|
||||
title: "mixing edge groups that are already applied and new edge groups",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
groupNames: []string{"edge group1", "edge group2", "edge group3", "edge group4", "edge group5", "edge group6"},
|
||||
endpointGroupNames: []string{"edge group1", "edge group2", "edge group3"},
|
||||
groupsToApply: []string{"edge group2", "edge group4", "edge group5"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.title, func(t *testing.T) {
|
||||
testFn(t, testCase)
|
||||
})
|
||||
}
|
||||
}
|
56
api/http/handler/endpoints/utils_update_tags.go
Normal file
56
api/http/handler/endpoints/utils_update_tags.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/set"
|
||||
)
|
||||
|
||||
// updateEnvironmentTags updates the tags associated to an environment
|
||||
func updateEnvironmentTags(tx dataservices.DataStoreTx, newTags []portainer.TagID, oldTags []portainer.TagID, environmentID portainer.EndpointID) (bool, error) {
|
||||
payloadTagSet := set.ToSet(newTags)
|
||||
environmentTagSet := set.ToSet(oldTags)
|
||||
union := set.Union(payloadTagSet, environmentTagSet)
|
||||
intersection := set.Intersection(payloadTagSet, environmentTagSet)
|
||||
|
||||
if len(union) <= len(intersection) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
updateSet := func(tagIDs set.Set[portainer.TagID], updateItem func(*portainer.Tag)) error {
|
||||
for tagID := range tagIDs {
|
||||
tag, err := tx.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to find a tag inside the database")
|
||||
}
|
||||
|
||||
updateItem(tag)
|
||||
|
||||
err = tx.Tag().UpdateTag(tagID, tag)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "Unable to persist tag changes inside the database")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
removeTags := environmentTagSet.Difference(payloadTagSet)
|
||||
err := updateSet(removeTags, func(tag *portainer.Tag) {
|
||||
delete(tag.Endpoints, environmentID)
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
addTags := payloadTagSet.Difference(environmentTagSet)
|
||||
err = updateSet(addTags, func(tag *portainer.Tag) {
|
||||
tag.Endpoints[environmentID] = true
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
165
api/http/handler/endpoints/utils_update_tags_test.go
Normal file
165
api/http/handler/endpoints/utils_update_tags_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_updateTags(t *testing.T) {
|
||||
|
||||
createTags := func(store *datastore.Store, tagNames []string) ([]portainer.Tag, error) {
|
||||
tags := make([]portainer.Tag, len(tagNames))
|
||||
for index, tagName := range tagNames {
|
||||
tag := &portainer.Tag{
|
||||
Name: tagName,
|
||||
Endpoints: make(map[portainer.EndpointID]bool),
|
||||
EndpointGroups: make(map[portainer.EndpointGroupID]bool),
|
||||
}
|
||||
|
||||
err := store.Tag().Create(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags[index] = *tag
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
checkTags := func(store *datastore.Store, is *assert.Assertions, tagIDs []portainer.TagID, endpointID portainer.EndpointID) {
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := store.Tag().Tag(tagID)
|
||||
is.NoError(err)
|
||||
|
||||
_, ok := tag.Endpoints[endpointID]
|
||||
is.True(ok, "expected endpoint to be tagged")
|
||||
}
|
||||
}
|
||||
|
||||
tagsByName := func(tags []portainer.Tag, tagNames []string) []portainer.Tag {
|
||||
result := make([]portainer.Tag, len(tagNames))
|
||||
for i, tagName := range tagNames {
|
||||
for j, tag := range tags {
|
||||
if tag.Name == tagName {
|
||||
result[i] = tags[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
getIDs := func(tags []portainer.Tag) []portainer.TagID {
|
||||
ids := make([]portainer.TagID, len(tags))
|
||||
for i, tag := range tags {
|
||||
ids[i] = tag.ID
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
title string
|
||||
endpoint *portainer.Endpoint
|
||||
tagNames []string
|
||||
endpointTagNames []string
|
||||
tagsToApply []string
|
||||
shouldNotBeUpdated bool
|
||||
}
|
||||
|
||||
testFn := func(t *testing.T, testCase testCase) {
|
||||
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
err := store.Endpoint().Create(testCase.endpoint)
|
||||
is.NoError(err)
|
||||
|
||||
tags, err := createTags(store, testCase.tagNames)
|
||||
is.NoError(err)
|
||||
|
||||
endpointTags := tagsByName(tags, testCase.endpointTagNames)
|
||||
for _, tag := range endpointTags {
|
||||
tag.Endpoints[testCase.endpoint.ID] = true
|
||||
|
||||
err = store.Tag().UpdateTag(tag.ID, &tag)
|
||||
is.NoError(err)
|
||||
}
|
||||
|
||||
endpointTagIDs := getIDs(endpointTags)
|
||||
testCase.endpoint.TagIDs = endpointTagIDs
|
||||
err = store.Endpoint().UpdateEndpoint(testCase.endpoint.ID, testCase.endpoint)
|
||||
is.NoError(err)
|
||||
|
||||
expectedTags := tagsByName(tags, testCase.tagsToApply)
|
||||
expectedTagIDs := make([]portainer.TagID, len(expectedTags))
|
||||
for i, tag := range expectedTags {
|
||||
expectedTagIDs[i] = tag.ID
|
||||
}
|
||||
|
||||
err = store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
updated, err := updateEnvironmentTags(tx, expectedTagIDs, testCase.endpoint.TagIDs, testCase.endpoint.ID)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(testCase.shouldNotBeUpdated, !updated)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
checkTags(store, is, expectedTagIDs, testCase.endpoint.ID)
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
title: "applying tags to an endpoint without tags",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
tagNames: []string{"tag1", "tag2", "tag3"},
|
||||
endpointTagNames: []string{},
|
||||
tagsToApply: []string{"tag1", "tag2", "tag3"},
|
||||
},
|
||||
{
|
||||
title: "applying tags to an endpoint with tags",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
tagNames: []string{"tag1", "tag2", "tag3", "tag4", "tag5", "tag6"},
|
||||
endpointTagNames: []string{"tag1", "tag2", "tag3"},
|
||||
tagsToApply: []string{"tag4", "tag5", "tag6"},
|
||||
},
|
||||
{
|
||||
title: "applying tags to an endpoint with tags that are already applied",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
tagNames: []string{"tag1", "tag2", "tag3"},
|
||||
endpointTagNames: []string{"tag1", "tag2", "tag3"},
|
||||
tagsToApply: []string{"tag1", "tag2", "tag3"},
|
||||
shouldNotBeUpdated: true,
|
||||
},
|
||||
{
|
||||
title: "adding new tags to an endpoint with tags ",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
tagNames: []string{"tag1", "tag2", "tag3", "tag4", "tag5", "tag6"},
|
||||
endpointTagNames: []string{"tag1", "tag2", "tag3"},
|
||||
tagsToApply: []string{"tag1", "tag2", "tag3", "tag4", "tag5", "tag6"},
|
||||
},
|
||||
{
|
||||
title: "mixing tags that are already applied and new tags",
|
||||
endpoint: &portainer.Endpoint{},
|
||||
tagNames: []string{"tag1", "tag2", "tag3", "tag4", "tag5", "tag6"},
|
||||
endpointTagNames: []string{"tag1", "tag2", "tag3"},
|
||||
tagsToApply: []string{"tag2", "tag4", "tag5"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.title, func(t *testing.T) {
|
||||
testFn(t, testCase)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue