mirror of https://github.com/portainer/portainer
task(endpoints): change the definition of /endpoints/remove EE-7126 (#11872)
parent
11404aaecb
commit
3b95c333fc
|
@ -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"`
|
||||||
Name string `json:"name"`
|
|
||||||
DeleteCluster bool `json:"deleteCluster"`
|
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…
Reference in New Issue