task(endpoints): change the definition of /endpoints/remove EE-7126 (#11872)

pull/11875/head
andres-portainer 6 months ago committed by GitHub
parent 11404aaecb
commit 3b95c333fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -18,43 +18,41 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type DeleteMultiplePayload struct { type endpointDeleteRequest struct {
Endpoints []struct { ID int `json:"id"`
ID int `json:"id"` DeleteCluster bool `json:"deleteCluster"`
Name string `json:"name"`
DeleteCluster bool `json:"deleteCluster"`
} `json:"environments"`
} }
func (payload *DeleteMultiplePayload) Validate(r *http.Request) error { type endpointDeleteBatchPayload struct {
Endpoints []endpointDeleteRequest `json:"endpoints"`
}
type endpointDeleteBatchPartialResponse struct {
Deleted []int `json:"deleted"`
Errors []int `json:"errors"`
}
func (payload *endpointDeleteBatchPayload) Validate(r *http.Request) error {
if payload == nil || len(payload.Endpoints) == 0 { if payload == nil || len(payload.Endpoints) == 0 {
return fmt.Errorf("invalid request payload; you must provide a list of nodes to delete") return fmt.Errorf("invalid request payload. You must provide a list of environments to delete")
} }
return nil return nil
} }
type DeleteMultipleResp struct {
Name string `json:"name"`
Err error `json:"err"`
}
// @id EndpointDelete // @id EndpointDelete
// @summary Remove an environment(endpoint) // @summary Remove an environment
// @description Remove an environment(endpoint). // @description Remove the environment associated to the specified identifier and optionally clean-up associated resources.
// @description **Access policy**: administrator // @description **Access policy**: Administrator only.
// @tags endpoints // @tags endpoints
// @security ApiKeyAuth // @security ApiKeyAuth || jwt
// @security jwt
// @param id path int true "Environment(Endpoint) identifier" // @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Success" // @success 204 "Environment successfully deleted."
// @failure 400 "Invalid request" // @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 403 "Permission denied" // @failure 403 "Unauthorized access or operation not allowed."
// @failure 404 "Environment(Endpoint) not found" // @failure 404 "Unable to find the environment with the specified identifier inside the database."
// @failure 500 "Server error" // @failure 500 "Server error occurred while attempting to delete the environment."
// @router /endpoints/{id} [delete] // @router /endpoints/{id} [delete]
// @deprecated
// Deprecated: use endpointDeleteMultiple instead.
func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil { if err != nil {
@ -86,58 +84,61 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return response.Empty(w) return response.Empty(w)
} }
// @id EndpointDeleteMultiple // @id EndpointDeleteBatch
// @summary Remove multiple environment(endpoint)s // @summary Remove multiple environments
// @description Remove multiple environment(endpoint)s. // @description Remove multiple environments and optionally clean-up associated resources.
// @description **Access policy**: administrator // @description **Access policy**: Administrator only.
// @tags endpoints // @tags endpoints
// @security ApiKeyAuth // @security ApiKeyAuth || jwt
// @security jwt
// @accept json // @accept json
// @produce json // @produce json
// @param body body DeleteMultiplePayload true "List of endpoints to delete" // @param body body endpointDeleteBatchPayload true "List of environments to delete, with optional deleteCluster flag to clean-up assocaited resources (cloud environments only)"
// @success 204 "Success" // @success 204 "Environment(s) successfully deleted."
// @failure 400 "Invalid request" // @failure 207 {object} endpointDeleteBatchPartialResponse "Partial success. Some environments were deleted successfully, while others failed."
// @failure 403 "Permission denied" // @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria."
// @failure 404 "Environment(Endpoint) not found" // @failure 403 "Unauthorized access or operation not allowed."
// @failure 500 "Server error" // @failure 500 "Server error occurred while attempting to delete the specified environments."
// @router /endpoints/remove [post] // @router /endpoints [delete]
func (handler *Handler) endpointDeleteMultiple(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { func (handler *Handler) endpointDeleteBatch(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var p DeleteMultiplePayload var p endpointDeleteBatchPayload
if err := request.DecodeAndValidateJSONPayload(r, &p); err != nil { if err := request.DecodeAndValidateJSONPayload(r, &p); err != nil {
return httperror.BadRequest("Invalid request payload", err) return httperror.BadRequest("Invalid request payload", err)
} }
var resps []DeleteMultipleResp resp := endpointDeleteBatchPartialResponse{
Deleted: []int{},
Errors: []int{},
}
err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error { if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
for _, e := range p.Endpoints { for _, e := range p.Endpoints {
// Demo endpoints cannot be deleted.
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(e.ID)) { if handler.demoService.IsDemoEnvironment(portainer.EndpointID(e.ID)) {
resps = append(resps, DeleteMultipleResp{ resp.Errors = append(resp.Errors, e.ID)
Name: e.Name, log.Warn().Err(httperrors.ErrNotAvailableInDemo).Msgf("Unable to remove demo environment %d", e.ID)
Err: httperrors.ErrNotAvailableInDemo,
})
continue continue
} }
// Attempt deletion. if err := handler.deleteEndpoint(tx, portainer.EndpointID(e.ID), e.DeleteCluster); err != nil {
err := handler.deleteEndpoint( resp.Errors = append(resp.Errors, e.ID)
tx, log.Warn().Err(err).Int("environment_id", e.ID).Msg("Unable to remove environment")
portainer.EndpointID(e.ID),
e.DeleteCluster,
)
resps = append(resps, DeleteMultipleResp{Name: e.Name, Err: err}) continue
}
resp.Deleted = append(resp.Deleted, e.ID)
} }
return nil return nil
}) }); err != nil {
if err != nil {
return httperror.InternalServerError("Unable to delete environments", err) return httperror.InternalServerError("Unable to delete environments", err)
} }
return response.JSON(w, resps) if len(resp.Errors) > 0 {
return response.JSONWithStatus(w, resp, http.StatusPartialContent)
}
return response.Empty(w)
} }
func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID portainer.EndpointID, deleteCluster bool) error { func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID portainer.EndpointID, deleteCluster bool) error {

@ -71,8 +71,8 @@ func NewHandler(bouncer security.BouncerService, demoService *demo.Service) *Han
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}", h.Handle("/endpoints/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/remove", h.Handle("/endpoints",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteMultiple))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDeleteBatch))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/dockerhub/{registryId}", h.Handle("/endpoints/{id}/dockerhub/{registryId}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}/snapshot", h.Handle("/endpoints/{id}/snapshot",

@ -18,30 +18,32 @@ export function useDeleteEnvironmentsMutation() {
deleteCluster?: boolean; deleteCluster?: boolean;
}[] }[]
) => { ) => {
const resps = await deleteEnvironments(environments); const resp = await deleteEnvironments(environments);
const successfulDeletions = resps.filter((r) => r.err === null);
const failedDeletions = resps.filter((r) => r.err !== null); if (resp === null) {
return { successfulDeletions, failedDeletions }; return { deleted: environments, errors: [] };
}
return {
deleted: environments.filter((e) =>
(resp.deleted || []).includes(e.id)
),
errors: environments.filter((e) => (resp.errors || []).includes(e.id)),
};
}, },
{ {
...withError('Unable to delete environment(s)'), ...withError('Unable to delete environment(s)'),
onSuccess: ({ successfulDeletions, failedDeletions }) => { onSuccess: ({ deleted, errors }) => {
queryClient.invalidateQueries(['environments']); queryClient.invalidateQueries(['environments']);
// show an error message for each env that failed to delete // show an error message for each env that failed to delete
failedDeletions.forEach((deletion) => { errors.forEach((e) => {
notifyError( notifyError(`Failed to remove environment ${e.name}`, undefined);
`Failed to remove environment`,
new Error(deletion.err ? deletion.err.Message : '') as Error
);
}); });
// show one summary message for all successful deletes // show one summary message for all successful deletes
if (successfulDeletions.length) { if (deleted.length) {
notifySuccess( notifySuccess(
`${pluralize( `${pluralize(deleted.length, 'Environment')} successfully removed`,
successfulDeletions.length, deleted.map((d) => d.name).join(', ')
'Environment'
)} successfully removed`,
successfulDeletions.map((deletion) => deletion.name).join(', ')
); );
} }
}, },
@ -53,10 +55,11 @@ async function deleteEnvironments(
environments: { id: EnvironmentId; deleteCluster?: boolean }[] environments: { id: EnvironmentId; deleteCluster?: boolean }[]
) { ) {
try { try {
const { data } = await axios.post< const { data } = await axios.delete<{
{ name: string; err: { Message: string } | null }[] deleted: EnvironmentId[];
>(buildUrl(undefined, 'remove'), { errors: EnvironmentId[];
environments, } | null>(buildUrl(), {
data: { endpoints: environments },
}); });
return data; return data;
} catch (e) { } catch (e) {

Loading…
Cancel
Save