chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type endpointGroupCreatePayload struct {
|
||||
// Environment(Endpoint) group name
|
||||
Name string `validate:"required" example:"my-environment-group"`
|
||||
// Environment(Endpoint) group description
|
||||
Description string `example:"description"`
|
||||
// List of environment(endpoint) identifiers that will be part of this group
|
||||
AssociatedEndpoints []portainer.EndpointID `example:"1,3"`
|
||||
// List of tag identifiers to which this environment(endpoint) group is associated
|
||||
TagIDs []portainer.TagID `example:"1,2"`
|
||||
}
|
||||
|
||||
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
|
||||
if len(payload.Name) == 0 {
|
||||
return errors.New("invalid environment group name")
|
||||
}
|
||||
|
||||
if payload.TagIDs == nil {
|
||||
payload.TagIDs = []portainer.TagID{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @summary Create an Environment(Endpoint) Group
|
||||
// @description Create a new environment(endpoint) group.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body endpointGroupCreatePayload true "Environment(Endpoint) Group details"
|
||||
// @success 200 {object} portainer.EndpointGroup "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups [post]
|
||||
func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload endpointGroupCreatePayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
var endpointGroup *portainer.EndpointGroup
|
||||
var err error
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
endpointGroup, err = handler.createEndpointGroup(tx, payload)
|
||||
return err
|
||||
})
|
||||
|
||||
return response.TxResponse(w, endpointGroup, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpointGroup(tx dataservices.DataStoreTx, payload endpointGroupCreatePayload) (*portainer.EndpointGroup, error) {
|
||||
endpointGroup := &portainer.EndpointGroup{
|
||||
Name: payload.Name,
|
||||
Description: payload.Description,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: payload.TagIDs,
|
||||
}
|
||||
|
||||
err := tx.EndpointGroup().Create(endpointGroup)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist the environment group inside the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
for _, id := range payload.AssociatedEndpoints {
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.ID == id {
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err := tx.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(tx, &endpoint, endpointGroup)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
tag, err := tx.Tag().Read(tagID)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
}
|
||||
|
||||
tag.EndpointGroups[endpointGroup.ID] = true
|
||||
|
||||
err = tx.Tag().Update(tagID, tag)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
return endpointGroup, nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EndpointGroupDelete
|
||||
// @summary Remove an environment(endpoint) group
|
||||
// @description Remove an environment(endpoint) group.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/{id} [delete]
|
||||
func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment group identifier route variable", err)
|
||||
}
|
||||
|
||||
if endpointGroupID == 1 {
|
||||
return httperror.Forbidden("Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default environment group"))
|
||||
}
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.deleteEndpointGroup(tx, portainer.EndpointGroupID(endpointGroupID))
|
||||
})
|
||||
|
||||
return response.TxEmptyResponse(w, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteEndpointGroup(tx dataservices.DataStoreTx, endpointGroupID portainer.EndpointGroupID) error {
|
||||
endpointGroup, err := tx.EndpointGroup().Read(endpointGroupID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if err := tx.EndpointGroup().Delete(endpointGroupID); err != nil {
|
||||
return httperror.InternalServerError("Unable to remove the environment group from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve environment from the database", err)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID != endpointGroupID {
|
||||
continue
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint); err != nil {
|
||||
return httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
|
||||
if err := handler.updateEndpointRelations(tx, &endpoint, nil); err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
tag, err := tx.Tag().Read(tagID)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
}
|
||||
|
||||
delete(tag.EndpointGroups, endpointGroup.ID)
|
||||
|
||||
if err := tx.Tag().Update(tagID, tag); err != nil {
|
||||
return httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EndpointGroupAddEndpoint
|
||||
// @summary Add an environment(endpoint) to an environment(endpoint) group
|
||||
// @description Add an environment(endpoint) to an environment(endpoint) group
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @param endpointId path int true "Environment(Endpoint) identifier"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/{id}/endpoints/{endpointId} [put]
|
||||
func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment group identifier route variable", err)
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment identifier route variable", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.addEndpoint(tx, portainer.EndpointGroupID(endpointGroupID), portainer.EndpointID(endpointID))
|
||||
})
|
||||
|
||||
return response.TxEmptyResponse(w, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) addEndpoint(tx dataservices.DataStoreTx, endpointGroupID portainer.EndpointGroupID, endpointID portainer.EndpointID) error {
|
||||
endpointGroup, err := tx.EndpointGroup().Read(endpointGroupID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err = tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment changes inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(tx, endpoint, endpointGroup)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id EndpointGroupDeleteEndpoint
|
||||
// @summary Removes environment(endpoint) from an environment(endpoint) group
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @param endpointId path int true "Environment(Endpoint) identifier"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/{id}/endpoints/{endpointId} [delete]
|
||||
func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment group identifier route variable", err)
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment identifier route variable", err)
|
||||
}
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return handler.removeEndpoint(tx, portainer.EndpointGroupID(endpointGroupID), portainer.EndpointID(endpointID))
|
||||
})
|
||||
|
||||
return response.TxEmptyResponse(w, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) removeEndpoint(tx dataservices.DataStoreTx, endpointGroupID portainer.EndpointGroupID, endpointID portainer.EndpointID) error {
|
||||
ok, err := tx.EndpointGroup().Exists(endpointGroupID)
|
||||
if !ok {
|
||||
return httperror.NotFound("Unable to find an environment group with the specified identifier inside the database", dserrors.ErrObjectNotFound)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
|
||||
err = tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment changes inside the database", err)
|
||||
}
|
||||
|
||||
err = handler.updateEndpointRelations(tx, endpoint, nil)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @summary Inspect an Environment(Endpoint) group
|
||||
// @description Retrieve details abont an environment(endpoint) group.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) group identifier"
|
||||
// @param size query boolean false "If true, include the number of environments and breakdown by type"
|
||||
// @success 200 {object} EndpointGroupResponse "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/{id} [get]
|
||||
func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment group identifier route variable", err)
|
||||
}
|
||||
|
||||
includeSize, err := request.RetrieveBooleanQueryParameter(r, "size", true)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: size", err)
|
||||
}
|
||||
|
||||
groupID := portainer.EndpointGroupID(endpointGroupID)
|
||||
|
||||
var endpointGroup *portainer.EndpointGroup
|
||||
var endpoints []portainer.Endpoint
|
||||
|
||||
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var err error
|
||||
endpointGroup, err = tx.EndpointGroup().Read(groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if includeSize {
|
||||
endpoints, err = tx.Endpoint().Endpoints()
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
}
|
||||
return httperror.InternalServerError("Unable to retrieve environment group details", err)
|
||||
}
|
||||
|
||||
resp := EndpointGroupResponse{
|
||||
EndpointGroup: *endpointGroup,
|
||||
}
|
||||
|
||||
if includeSize {
|
||||
countMap, typeInfoMap := computeGroupSizeInfo([]portainer.EndpointGroup{*endpointGroup}, endpoints)
|
||||
resp.Total = countMap[groupID]
|
||||
resp.TypeInfo = typeInfoMap[groupID]
|
||||
}
|
||||
|
||||
return response.JSON(w, resp)
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/set"
|
||||
endpointutils "github.com/portainer/portainer/pkg/endpoints"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
func computeGroupSizeInfo(endpointGroups []portainer.EndpointGroup, endpoints []portainer.Endpoint) (map[portainer.EndpointGroupID]int, map[portainer.EndpointGroupID]EndpointGroupTypeInfo) {
|
||||
groupSet := set.Set[portainer.EndpointGroupID]{}
|
||||
for i := range endpointGroups {
|
||||
groupSet[endpointGroups[i].ID] = true
|
||||
}
|
||||
|
||||
countMap := make(map[portainer.EndpointGroupID]int)
|
||||
typeInfoMap := make(map[portainer.EndpointGroupID]EndpointGroupTypeInfo)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if _, ok := groupSet[endpoint.GroupID]; !ok {
|
||||
continue
|
||||
}
|
||||
countMap[endpoint.GroupID]++
|
||||
|
||||
typeInfo := typeInfoMap[endpoint.GroupID]
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
typeInfo.Kubernetes++
|
||||
} else if endpoint.ContainerEngine == portainer.ContainerEnginePodman {
|
||||
typeInfo.Podman++
|
||||
} else {
|
||||
typeInfo.Docker++
|
||||
}
|
||||
typeInfoMap[endpoint.GroupID] = typeInfo
|
||||
}
|
||||
|
||||
for groupID, typeInfo := range typeInfoMap {
|
||||
var bits int
|
||||
if typeInfo.Docker > 0 {
|
||||
bits |= 1
|
||||
}
|
||||
if typeInfo.Kubernetes > 0 {
|
||||
bits |= 2
|
||||
}
|
||||
if typeInfo.Podman > 0 {
|
||||
bits |= 4
|
||||
}
|
||||
typeInfo.Mixed = bits&(bits-1) != 0
|
||||
typeInfoMap[groupID] = typeInfo
|
||||
}
|
||||
|
||||
return countMap, typeInfoMap
|
||||
}
|
||||
|
||||
type EndpointGroupTypeInfo struct {
|
||||
Docker int `json:"Docker" validate:"required"`
|
||||
Kubernetes int `json:"Kubernetes" validate:"required"`
|
||||
Podman int `json:"Podman" validate:"required"`
|
||||
Mixed bool `json:"Mixed" validate:"required"`
|
||||
}
|
||||
|
||||
type EndpointGroupResponse struct {
|
||||
portainer.EndpointGroup
|
||||
Total int `json:"Total,omitzero"`
|
||||
TypeInfo EndpointGroupTypeInfo `json:"TypeInfo,omitzero"`
|
||||
}
|
||||
|
||||
// @id EndpointGroupList
|
||||
// @summary List Environment(Endpoint) groups
|
||||
// @description List all environment(endpoint) groups based on the current user authorizations. Will
|
||||
// @description return all environment(endpoint) groups if using an administrator account otherwise it will
|
||||
// @description only return authorized environment(endpoint) groups.
|
||||
// @description **Access policy**: restricted
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param size query boolean false "If true, each environment(endpoint) group will include the number of environments(endpoints) associated to it and breakdown by type"
|
||||
// @success 200 {array} EndpointGroupResponse "Environment(Endpoint) group"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups [get]
|
||||
func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
includeSize, err := request.RetrieveBooleanQueryParameter(r, "size", true)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: size", err)
|
||||
}
|
||||
|
||||
var endpoints []portainer.Endpoint
|
||||
var endpointGroups []portainer.EndpointGroup
|
||||
var handlerErr *httperror.HandlerError
|
||||
if err := handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
var txErr error
|
||||
endpointGroups, txErr = handler.DataStore.EndpointGroup().ReadAll()
|
||||
if txErr != nil {
|
||||
handlerErr = httperror.InternalServerError("Unable to retrieve environment groups from the database", txErr)
|
||||
return handlerErr
|
||||
}
|
||||
|
||||
if includeSize {
|
||||
endpoints, txErr = tx.Endpoint().Endpoints()
|
||||
if txErr != nil {
|
||||
handlerErr = httperror.InternalServerError("Unable to retrieve endpoints from the database", txErr)
|
||||
return handlerErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
if handlerErr != nil {
|
||||
return handlerErr
|
||||
}
|
||||
return httperror.InternalServerError("Unable to retrieve data from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
endpointGroups = security.FilterEndpointGroups(endpointGroups, securityContext)
|
||||
|
||||
if len(endpointGroups) == 0 {
|
||||
return response.JSON(w, []portainer.EndpointGroup{})
|
||||
}
|
||||
|
||||
var endpointGroupCountMap map[portainer.EndpointGroupID]int
|
||||
var endpointGroupTypeInfoMap map[portainer.EndpointGroupID]EndpointGroupTypeInfo
|
||||
if includeSize {
|
||||
endpointGroupCountMap, endpointGroupTypeInfoMap = computeGroupSizeInfo(endpointGroups, endpoints)
|
||||
}
|
||||
|
||||
endpointGroupsResponse := make([]EndpointGroupResponse, len(endpointGroups))
|
||||
for i := range endpointGroups {
|
||||
groupID := endpointGroups[i].ID
|
||||
|
||||
endpointGroupsResponse[i] = EndpointGroupResponse{
|
||||
EndpointGroup: endpointGroups[i],
|
||||
}
|
||||
|
||||
if includeSize {
|
||||
endpointGroupsResponse[i].Total = endpointGroupCountMap[groupID]
|
||||
endpointGroupsResponse[i].TypeInfo = endpointGroupTypeInfoMap[groupID]
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroupsResponse)
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHandler_endpointGroupList(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, false)
|
||||
handler := setUpHandler(t, store)
|
||||
|
||||
groups := setUpGroups(t, store)
|
||||
|
||||
t.Run("with groups, no size flag", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/endpoint_groups", nil)
|
||||
rrc := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
res := make([]EndpointGroupResponse, 0)
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&res))
|
||||
require.Len(t, res, len(groups)+1, "should contain an additional default group")
|
||||
for _, group := range res {
|
||||
assert.Zero(t, group.Total)
|
||||
assert.Zero(t, group.TypeInfo.Docker)
|
||||
assert.Zero(t, group.TypeInfo.Kubernetes)
|
||||
assert.Zero(t, group.TypeInfo.Podman)
|
||||
assert.False(t, group.TypeInfo.Mixed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with size flag, no endpoints", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/endpoint_groups?size=true", nil)
|
||||
rrc := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
res := make([]EndpointGroupResponse, 0)
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&res))
|
||||
for _, group := range res {
|
||||
assert.Zero(t, group.Total)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with size flag and single docker endpoint", func(t *testing.T) {
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: 1,
|
||||
GroupID: groups[0].ID,
|
||||
Type: portainer.DockerEnvironment,
|
||||
}
|
||||
require.NoError(t, store.Endpoint().Create(endpoint))
|
||||
t.Cleanup(func() { _ = store.Endpoint().DeleteEndpoint(endpoint.ID) })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/endpoint_groups?size=true", nil)
|
||||
rrc := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
res := make([]EndpointGroupResponse, 0)
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&res))
|
||||
|
||||
var group1 *EndpointGroupResponse
|
||||
for i := range res {
|
||||
if res[i].ID == groups[0].ID {
|
||||
group1 = &res[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, group1)
|
||||
assert.Equal(t, 1, group1.Total)
|
||||
assert.Equal(t, 1, group1.TypeInfo.Docker)
|
||||
assert.Equal(t, 0, group1.TypeInfo.Kubernetes)
|
||||
assert.Equal(t, 0, group1.TypeInfo.Podman)
|
||||
assert.False(t, group1.TypeInfo.Mixed)
|
||||
})
|
||||
|
||||
t.Run("with mixed endpoint types", func(t *testing.T) {
|
||||
dockerEndpoint := &portainer.Endpoint{
|
||||
ID: 2,
|
||||
GroupID: groups[1].ID,
|
||||
Type: portainer.DockerEnvironment,
|
||||
}
|
||||
require.NoError(t, store.Endpoint().Create(dockerEndpoint))
|
||||
t.Cleanup(func() { _ = store.Endpoint().DeleteEndpoint(dockerEndpoint.ID) })
|
||||
|
||||
k8sEndpoint := &portainer.Endpoint{
|
||||
ID: 3,
|
||||
GroupID: groups[1].ID,
|
||||
Type: portainer.KubernetesLocalEnvironment,
|
||||
}
|
||||
require.NoError(t, store.Endpoint().Create(k8sEndpoint))
|
||||
t.Cleanup(func() { _ = store.Endpoint().DeleteEndpoint(k8sEndpoint.ID) })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/endpoint_groups?size=true", nil)
|
||||
rrc := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
res := make([]EndpointGroupResponse, 0)
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&res))
|
||||
|
||||
var group2 *EndpointGroupResponse
|
||||
for i := range res {
|
||||
if res[i].ID == groups[1].ID {
|
||||
group2 = &res[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, group2)
|
||||
assert.Equal(t, 2, group2.Total)
|
||||
assert.Equal(t, 1, group2.TypeInfo.Docker)
|
||||
assert.Equal(t, 1, group2.TypeInfo.Kubernetes)
|
||||
assert.Equal(t, 0, group2.TypeInfo.Podman)
|
||||
assert.True(t, group2.TypeInfo.Mixed, "should be marked as mixed when multiple types exist")
|
||||
})
|
||||
|
||||
t.Run("with podman endpoint", func(t *testing.T) {
|
||||
dockerEndpoint := &portainer.Endpoint{
|
||||
ID: 4,
|
||||
GroupID: groups[0].ID,
|
||||
Type: portainer.DockerEnvironment,
|
||||
}
|
||||
require.NoError(t, store.Endpoint().Create(dockerEndpoint))
|
||||
t.Cleanup(func() { _ = store.Endpoint().DeleteEndpoint(dockerEndpoint.ID) })
|
||||
|
||||
podmanEndpoint := &portainer.Endpoint{
|
||||
ID: 5,
|
||||
GroupID: groups[0].ID,
|
||||
Type: portainer.DockerEnvironment,
|
||||
ContainerEngine: portainer.ContainerEnginePodman,
|
||||
}
|
||||
require.NoError(t, store.Endpoint().Create(podmanEndpoint))
|
||||
t.Cleanup(func() { _ = store.Endpoint().DeleteEndpoint(podmanEndpoint.ID) })
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/endpoint_groups?size=true", nil)
|
||||
rrc := &security.RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
}
|
||||
req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc))
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
res := make([]EndpointGroupResponse, 0)
|
||||
require.NoError(t, json.NewDecoder(w.Body).Decode(&res))
|
||||
|
||||
var group1 *EndpointGroupResponse
|
||||
for i := range res {
|
||||
if res[i].ID == groups[0].ID {
|
||||
group1 = &res[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, group1)
|
||||
assert.Equal(t, 2, group1.Total)
|
||||
assert.Equal(t, 1, group1.TypeInfo.Docker)
|
||||
assert.Equal(t, 0, group1.TypeInfo.Kubernetes)
|
||||
assert.Equal(t, 1, group1.TypeInfo.Podman)
|
||||
assert.True(t, group1.TypeInfo.Mixed)
|
||||
})
|
||||
}
|
||||
|
||||
func setUpGroups(t *testing.T, store *datastore.Store) []portainer.EndpointGroup {
|
||||
group1 := &portainer.EndpointGroup{
|
||||
ID: 1,
|
||||
Name: "Group 1",
|
||||
}
|
||||
group2 := &portainer.EndpointGroup{
|
||||
ID: 2,
|
||||
Name: "Group 2",
|
||||
}
|
||||
require.NoError(t, store.EndpointGroup().Create(group1))
|
||||
require.NoError(t, store.EndpointGroup().Create(group2))
|
||||
|
||||
return []portainer.EndpointGroup{*group1, *group2}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/pendingactions/handlers"
|
||||
"github.com/portainer/portainer/api/tag"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type endpointGroupUpdatePayload struct {
|
||||
// Environment(Endpoint) group name
|
||||
Name string `example:"my-environment-group"`
|
||||
// Environment(Endpoint) group description
|
||||
Description *string `example:"description"`
|
||||
// List of environment(endpoint) identifiers that will be part of this group
|
||||
AssociatedEndpoints []portainer.EndpointID `example:"1,3"`
|
||||
// List of tag identifiers associated to the environment(endpoint) group
|
||||
TagIDs []portainer.TagID `example:"3,4"`
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
}
|
||||
|
||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id EndpointGroupUpdate
|
||||
// @summary Update an environment(endpoint) group
|
||||
// @description Update an environment(endpoint) group.
|
||||
// @description **Access policy**: administrator
|
||||
// @tags endpoint_groups
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param id path int true "EndpointGroup identifier"
|
||||
// @param body body endpointGroupUpdatePayload true "EndpointGroup details"
|
||||
// @success 200 {object} portainer.EndpointGroup "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "EndpointGroup not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoint_groups/{id} [put]
|
||||
func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid environment group identifier route variable", err)
|
||||
}
|
||||
|
||||
var payload endpointGroupUpdatePayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
var endpointGroup *portainer.EndpointGroup
|
||||
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
endpointGroup, err = handler.updateEndpointGroup(tx, portainer.EndpointGroupID(endpointGroupID), payload)
|
||||
return err
|
||||
})
|
||||
|
||||
return response.TxResponse(w, endpointGroup, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpointGroupID portainer.EndpointGroupID, payload endpointGroupUpdatePayload) (*portainer.EndpointGroup, error) {
|
||||
endpointGroup, err := tx.EndpointGroup().Read(endpointGroupID)
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil, httperror.NotFound("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to find an environment group with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
endpointGroup.Name = payload.Name
|
||||
}
|
||||
|
||||
if payload.Description != nil {
|
||||
endpointGroup.Description = *payload.Description
|
||||
}
|
||||
|
||||
tagsChanged := false
|
||||
if payload.TagIDs != nil {
|
||||
payloadTagSet := tag.Set(payload.TagIDs)
|
||||
endpointGroupTagSet := tag.Set((endpointGroup.TagIDs))
|
||||
union := tag.Union(payloadTagSet, endpointGroupTagSet)
|
||||
intersection := tag.IntersectionCount(payloadTagSet, endpointGroupTagSet)
|
||||
tagsChanged = len(union) > intersection
|
||||
|
||||
if tagsChanged {
|
||||
removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet)
|
||||
|
||||
for tagID := range removeTags {
|
||||
tag, err := tx.Tag().Read(tagID)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
}
|
||||
|
||||
delete(tag.EndpointGroups, endpointGroup.ID)
|
||||
|
||||
err = tx.Tag().Update(tagID, tag)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroup.TagIDs = payload.TagIDs
|
||||
for _, tagID := range payload.TagIDs {
|
||||
tag, err := tx.Tag().Read(tagID)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to find a tag inside the database", err)
|
||||
}
|
||||
|
||||
tag.EndpointGroups[endpointGroup.ID] = true
|
||||
|
||||
err = tx.Tag().Update(tagID, tag)
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist tag changes inside the database", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
|
||||
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
||||
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == endpointGroup.ID && endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
if err := handler.AuthorizationService.CleanNAPWithOverridePolicies(tx, &endpoint, endpointGroup); err != nil {
|
||||
// Update flag with endpoint and continue
|
||||
if err := handler.PendingActionsService.Create(tx, handlers.NewCleanNAPWithOverridePolicies(endpoint.ID, &endpointGroup.ID)); err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to create pending action to clean NAP with override policies for endpoint (%d) and endpoint group (%d).", endpoint.ID, endpointGroup.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.EndpointGroup().Update(endpointGroup.ID, endpointGroup); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist environment group changes inside the database", err)
|
||||
}
|
||||
|
||||
// Handle associated endpoints updates
|
||||
endpointsChanged := false
|
||||
if payload.AssociatedEndpoints != nil {
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
// Build a set of the new endpoint IDs for quick lookup
|
||||
newEndpointSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range payload.AssociatedEndpoints {
|
||||
newEndpointSet[id] = true
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
endpoint := &endpoints[i]
|
||||
wasInGroup := endpoint.GroupID == endpointGroup.ID
|
||||
shouldBeInGroup := newEndpointSet[endpoint.ID]
|
||||
|
||||
if wasInGroup && !shouldBeInGroup {
|
||||
// Remove from group (move to Unassigned)
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
if err := handler.updateEndpointRelations(tx, endpoint, nil); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
endpointsChanged = true
|
||||
} else if !wasInGroup && shouldBeInGroup {
|
||||
// Add to group
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
if err := tx.Endpoint().UpdateEndpoint(endpoint.ID, endpoint); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to update environment", err)
|
||||
}
|
||||
if err := handler.updateEndpointRelations(tx, endpoint, endpointGroup); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
endpointsChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconcile endpoints in the group if tags changed (but endpoints weren't already reconciled)
|
||||
if tagsChanged && !endpointsChanged {
|
||||
endpoints, err := tx.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == endpointGroup.ID {
|
||||
if err := handler.updateEndpointRelations(tx, &endpoint, endpointGroup); err != nil {
|
||||
return nil, httperror.InternalServerError("Unable to persist environment relations changes inside the database", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpointGroup, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
)
|
||||
|
||||
func (handler *Handler) updateEndpointRelations(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error {
|
||||
if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment {
|
||||
return nil
|
||||
}
|
||||
|
||||
if endpointGroup == nil {
|
||||
unassignedGroup, err := tx.EndpointGroup().Read(portainer.EndpointGroupID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup = unassignedGroup
|
||||
}
|
||||
|
||||
endpointRelation, err := tx.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := tx.EdgeGroup().ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := tx.EdgeStack().EdgeStacks()
|
||||
if err != nil {
|
||||
if tx.IsErrObjectNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
endpointStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
stacksSet := map[portainer.EdgeStackID]bool{}
|
||||
for _, edgeStackID := range endpointStacks {
|
||||
stacksSet[edgeStackID] = true
|
||||
}
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return tx.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/pendingactions"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore dataservices.DataStore
|
||||
PendingActionsService *pendingactions.PendingActionsService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) group operations.
|
||||
func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
)
|
||||
|
||||
func setUpHandler(t *testing.T, store *datastore.Store) *Handler {
|
||||
t.Helper()
|
||||
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||
handler.DataStore = store
|
||||
return handler
|
||||
}
|
||||
Reference in New Issue
Block a user