diff --git a/api/http/handler/endpoints/endpoint_association_delete.go b/api/http/handler/endpoints/endpoint_association_delete.go new file mode 100644 index 000000000..ad294c883 --- /dev/null +++ b/api/http/handler/endpoints/endpoint_association_delete.go @@ -0,0 +1,56 @@ +package endpoints + +import ( + "errors" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" +) + +// @id EndpointAssociationDelete +// @summary De-association an edge endpoint +// @description De-association an edge endpoint. +// @description **Access policy**: administrator +// @security jwt +// @tags endpoints +// @produce json +// @param id path int true "Endpoint identifier" +// @success 200 {object} portainer.Endpoint "Success" +// @failure 400 "Invalid request" +// @failure 404 "Endpoint not found" +// @failure 500 "Server error" +// @router /api/endpoints/:id/association [put] +func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint type", errors.New("Invalid endpoint type")} + } + + endpoint.EdgeID = "" + endpoint.Snapshots = []portainer.DockerSnapshot{} + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{} + + err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err} + } + + handler.ReverseTunnelService.SetTunnelStatusToIdle(endpoint.ID) + + return response.JSON(w, endpoint) +} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index bee2eb1d6..25f8d18a0 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -45,6 +45,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/settings", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut) + h.Handle("/endpoints/{id}/association", + bouncer.AdminAccess(httperror.LoggerHandler(h.endpointAssociationDelete))).Methods(http.MethodDelete) h.Handle("/endpoints/snapshot", bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) h.Handle("/endpoints", diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 52bc57f92..9e25024c2 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -16,6 +16,7 @@ angular.module('portainer.app').factory('Endpoints', [ }, get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, + deassociate: { method: 'DELETE', params: { id: '@id', action: 'association' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, remove: { method: 'DELETE', params: { id: '@id' } }, snapshots: { method: 'POST', params: { action: 'snapshot' } }, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index e272b8a08..33dd04e29 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -41,6 +41,10 @@ angular.module('portainer.app').factory('EndpointService', [ return Endpoints.updateAccess({ id: id }, { UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies }).$promise; }; + service.deassociateEndpoint = function (endpointID) { + return Endpoints.deassociate({ id: endpointID }).$promise; + }; + service.updateEndpoint = function (id, payload) { var deferred = $q.defer(); FileUploadService.uploadTLSFilesForEndpoint(id, payload.TLSCACert, payload.TLSCert, payload.TLSKey) diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 4baee3d82..7e03ba7bb 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -152,6 +152,24 @@ angular.module('portainer.app').factory('ModalService', [ }); }; + service.confirmDeassociate = function (callback) { + const message = + '
De-associating this Edge endpoint will mark it as non associated and will clear the registered Edge ID.
' + + 'Any agent started with the Edge key associated to this endpoint will be able to re-associate with this endpoint.
' + + 'You can re-use the Edge ID and Edge key that you used to deploy the existing Edge agent to associate a new Edge device to this endpoint.
'; + service.confirm({ + title: 'About de-associating', + message: $sanitize(message), + buttons: { + confirm: { + label: 'De-associate', + className: 'btn-primary', + }, + }, + callback: callback, + }); + }; + service.confirmUpdate = function (message, callback) { message = $sanitize(message); service.confirm({ diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 4b16b5078..defa4f9c4 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -22,6 +22,11 @@
Edge identifier: {{ endpoint.EdgeID }}
+ +