feat(edgegroups): add support for transactions EE-5323 (#8946)

pull/8949/head
andres-portainer 2023-05-16 16:07:03 -03:00 committed by GitHub
parent d29b688eb9
commit 1473cc208b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 38 deletions

View File

@ -2,16 +2,17 @@ package edgegroups
import ( import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
) )
type endpointSetType map[portainer.EndpointID]bool type endpointSetType map[portainer.EndpointID]bool
func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatch bool) ([]portainer.EndpointID, error) { func getEndpointsByTags(tx dataservices.DataStoreTx, tagIDs []portainer.TagID, partialMatch bool) ([]portainer.EndpointID, error) {
if len(tagIDs) == 0 { if len(tagIDs) == 0 {
return []portainer.EndpointID{}, nil return []portainer.EndpointID{}, nil
} }
endpoints, err := handler.DataStore.Endpoint().Endpoints() endpoints, err := tx.Endpoint().Endpoints()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -20,10 +21,11 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc
tags := []portainer.Tag{} tags := []portainer.Tag{}
for _, tagID := range tagIDs { for _, tagID := range tagIDs {
tag, err := handler.DataStore.Tag().Tag(tagID) tag, err := tx.Tag().Tag(tagID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tags = append(tags, *tag) tags = append(tags, *tag)
} }
@ -48,25 +50,31 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType { func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{} groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{}
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
groupID := endpoint.GroupID groupID := endpoint.GroupID
if groupEndpoints[groupID] == nil { if groupEndpoints[groupID] == nil {
groupEndpoints[groupID] = endpointSetType{} groupEndpoints[groupID] = endpointSetType{}
} }
groupEndpoints[groupID][endpoint.ID] = true groupEndpoints[groupID][endpoint.ID] = true
} }
return groupEndpoints return groupEndpoints
} }
func mapTagsToEndpoints(tags []portainer.Tag, groupEndpoints map[portainer.EndpointGroupID]endpointSetType) []endpointSetType { func mapTagsToEndpoints(tags []portainer.Tag, groupEndpoints map[portainer.EndpointGroupID]endpointSetType) []endpointSetType {
sets := []endpointSetType{} sets := []endpointSetType{}
for _, tag := range tags { for _, tag := range tags {
set := tag.Endpoints set := tag.Endpoints
for groupID := range tag.EndpointGroups { for groupID := range tag.EndpointGroups {
for endpointID := range groupEndpoints[groupID] { for endpointID := range groupEndpoints[groupID] {
set[endpointID] = true set[endpointID] = true
} }
} }
sets = append(sets, set) sets = append(sets, set)
} }

View File

@ -8,6 +8,8 @@ import (
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/pkg/featureflags"
) )
// @id EdgeGroupDelete // @id EdgeGroupDelete
@ -27,43 +29,64 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request)
return httperror.BadRequest("Invalid Edge group identifier route variable", err) return httperror.BadRequest("Invalid Edge group identifier route variable", err)
} }
_, err = handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) if featureflags.IsEnabled(portainer.FeatureNoTx) {
if handler.DataStore.IsErrObjectNotFound(err) { err = deleteEdgeGroup(handler.DataStore, portainer.EdgeGroupID(edgeGroupID))
} else {
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return deleteEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
})
}
if err != nil {
var httpErr *httperror.HandlerError
if errors.As(err, &httpErr) {
return httpErr
}
return httperror.InternalServerError("Unexpected error", err)
}
return response.Empty(w)
}
func deleteEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) error {
_, err := tx.EdgeGroup().EdgeGroup(ID)
if tx.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err) return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
} else if err != nil { } else if err != nil {
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err) return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
} }
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() edgeStacks, err := tx.EdgeStack().EdgeStacks()
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve Edge stacks from the database", err) return httperror.InternalServerError("Unable to retrieve Edge stacks from the database", err)
} }
for _, edgeStack := range edgeStacks { for _, edgeStack := range edgeStacks {
for _, groupID := range edgeStack.EdgeGroups { for _, groupID := range edgeStack.EdgeGroups {
if groupID == portainer.EdgeGroupID(edgeGroupID) { if groupID == ID {
return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge stack", errors.New("edge group is used by an Edge stack")) return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge stack", errors.New("edge group is used by an Edge stack"))
} }
} }
} }
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs() edgeJobs, err := tx.EdgeJob().EdgeJobs()
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve Edge jobs from the database", err) return httperror.InternalServerError("Unable to retrieve Edge jobs from the database", err)
} }
for _, edgeJob := range edgeJobs { for _, edgeJob := range edgeJobs {
for _, groupID := range edgeJob.EdgeGroups { for _, groupID := range edgeJob.EdgeGroups {
if groupID == portainer.EdgeGroupID(edgeGroupID) { if groupID == ID {
return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge job", errors.New("edge group is used by an Edge job")) return httperror.NewError(http.StatusConflict, "Edge group is used by an Edge job", errors.New("edge group is used by an Edge job"))
} }
} }
} }
err = handler.DataStore.EdgeGroup().DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID)) err = tx.EdgeGroup().DeleteEdgeGroup(ID)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to remove the Edge group from the database", err) return httperror.InternalServerError("Unable to remove the Edge group from the database", err)
} }
return response.Empty(w) return nil
} }

View File

@ -5,8 +5,9 @@ import (
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/pkg/featureflags"
) )
// @id EdgeGroupInspect // @id EdgeGroupInspect
@ -27,21 +28,35 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
return httperror.BadRequest("Invalid Edge group identifier route variable", err) return httperror.BadRequest("Invalid Edge group identifier route variable", err)
} }
edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) var edgeGroup *portainer.EdgeGroup
if handler.DataStore.IsErrObjectNotFound(err) { if featureflags.IsEnabled(portainer.FeatureNoTx) {
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err) edgeGroup, err = getEdgeGroup(handler.DataStore, portainer.EdgeGroupID(edgeGroupID))
} else {
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
edgeGroup, err = getEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
return err
})
}
return txResponse(w, edgeGroup, err)
}
func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
edgeGroup, err := tx.EdgeGroup().EdgeGroup(ID)
if tx.IsErrObjectNotFound(err) {
return nil, httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
} else if err != nil { } else if err != nil {
return httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err) return nil, httperror.InternalServerError("Unable to find an Edge group with the specified identifier inside the database", err)
} }
if edgeGroup.Dynamic { if edgeGroup.Dynamic {
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) endpoints, err := getEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err) return nil, httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err)
} }
edgeGroup.Endpoints = endpoints edgeGroup.Endpoints = endpoints
} }
return response.JSON(w, edgeGroup) return edgeGroup, err
} }

View File

@ -5,10 +5,10 @@ import (
"net/http" "net/http"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/slices" "github.com/portainer/portainer/api/internal/slices"
"github.com/portainer/portainer/pkg/featureflags"
) )
type decoratedEdgeGroup struct { type decoratedEdgeGroup struct {
@ -30,14 +30,30 @@ type decoratedEdgeGroup struct {
// @failure 503 "Edge compute features are disabled" // @failure 503 "Edge compute features are disabled"
// @router /edge_groups [get] // @router /edge_groups [get]
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() var decoratedEdgeGroups []decoratedEdgeGroup
if err != nil { var err error
return httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
if featureflags.IsEnabled(portainer.FeatureNoTx) {
decoratedEdgeGroups, err = getEdgeGroupList(handler.DataStore)
} else {
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
decoratedEdgeGroups, err = getEdgeGroupList(tx)
return err
})
} }
edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() return txResponse(w, decoratedEdgeGroups, err)
}
func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error) {
edgeGroups, err := tx.EdgeGroup().EdgeGroups()
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve Edge stacks from the database", err) return nil, httperror.InternalServerError("Unable to retrieve Edge groups from the database", err)
}
edgeStacks, err := tx.EdgeStack().EdgeStacks()
if err != nil {
return nil, httperror.InternalServerError("Unable to retrieve Edge stacks from the database", err)
} }
usedEdgeGroups := make(map[portainer.EdgeGroupID]bool) usedEdgeGroups := make(map[portainer.EdgeGroupID]bool)
@ -48,9 +64,9 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h
} }
} }
edgeJobs, err := handler.DataStore.EdgeJob().EdgeJobs() edgeJobs, err := tx.EdgeJob().EdgeJobs()
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve Edge jobs from the database", err) return nil, httperror.InternalServerError("Unable to retrieve Edge jobs from the database", err)
} }
decoratedEdgeGroups := []decoratedEdgeGroup{} decoratedEdgeGroups := []decoratedEdgeGroup{}
@ -68,35 +84,33 @@ func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *h
EndpointTypes: []portainer.EndpointType{}, EndpointTypes: []portainer.EndpointType{},
} }
if edgeGroup.Dynamic { if edgeGroup.Dynamic {
endpointIDs, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch) endpointIDs, err := getEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err) return nil, httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err)
} }
edgeGroup.Endpoints = endpointIDs edgeGroup.Endpoints = endpointIDs
} }
endpointTypes, err := getEndpointTypes(handler.DataStore.Endpoint(), edgeGroup.Endpoints) endpointTypes, err := getEndpointTypes(tx, edgeGroup.Endpoints)
if err != nil { if err != nil {
return httperror.InternalServerError("Unable to retrieve environment types for Edge group", err) return nil, httperror.InternalServerError("Unable to retrieve environment types for Edge group", err)
} }
edgeGroup.EndpointTypes = endpointTypes edgeGroup.EndpointTypes = endpointTypes
edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID] edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID]
edgeGroup.HasEdgeGroup = usedByEdgeJob edgeGroup.HasEdgeGroup = usedByEdgeJob
decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup) decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup)
} }
return response.JSON(w, decoratedEdgeGroups) return decoratedEdgeGroups, nil
} }
func getEndpointTypes(endpointService dataservices.EndpointService, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) { func getEndpointTypes(tx dataservices.DataStoreTx, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) {
typeSet := map[portainer.EndpointType]bool{} typeSet := map[portainer.EndpointType]bool{}
for _, endpointID := range endpointIds { for _, endpointID := range endpointIds {
endpoint, err := endpointService.Endpoint(endpointID) endpoint, err := tx.Endpoint().Endpoint(endpointID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed fetching environment: %w", err) return nil, fmt.Errorf("failed fetching environment: %w", err)
} }

View File

@ -38,7 +38,7 @@ func Test_getEndpointTypes(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
ans, err := getEndpointTypes(datastore.Endpoint(), test.endpointIds) ans, err := getEndpointTypes(datastore, test.endpointIds)
assert.NoError(t, err, "getEndpointTypes shouldn't fail") assert.NoError(t, err, "getEndpointTypes shouldn't fail")
assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans) assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
@ -48,6 +48,6 @@ func Test_getEndpointTypes(t *testing.T) {
func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) { func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) {
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{})) datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
_, err := getEndpointTypes(datastore.Endpoint(), []portainer.EndpointID{1}) _, err := getEndpointTypes(datastore, []portainer.EndpointID{1})
assert.Error(t, err, "getEndpointTypes should fail") assert.Error(t, err, "getEndpointTypes should fail")
} }

View File

@ -35,6 +35,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupUpdate)))).Methods(http.MethodPut) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupUpdate)))).Methods(http.MethodPut)
h.Handle("/edge_groups/{id}", h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete) bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeGroupDelete)))).Methods(http.MethodDelete)
return h return h
} }