feat: EE-424 Provide a way to re-associate an Edge endpoint to a new Edge agent (#5266)
Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/5387/head
parent
ce31de5e9e
commit
5652bac004
|
@ -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)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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' } },
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -152,6 +152,24 @@ angular.module('portainer.app').factory('ModalService', [
|
|||
});
|
||||
};
|
||||
|
||||
service.confirmDeassociate = function (callback) {
|
||||
const message =
|
||||
'<p>De-associating this Edge endpoint will mark it as non associated and will clear the registered Edge ID.</p>' +
|
||||
'<p>Any agent started with the Edge key associated to this endpoint will be able to re-associate with this endpoint.</p>' +
|
||||
'<p>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.</p>';
|
||||
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({
|
||||
|
|
|
@ -22,6 +22,11 @@
|
|||
<p>
|
||||
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress" ng-click="onDeassociateEndpoint()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress">De-associate</span>
|
||||
</button>
|
||||
</p>
|
||||
</span>
|
||||
</information-panel>
|
||||
<information-panel ng-if="state.edgeEndpoint && !endpoint.EdgeID" title-text="Deploy an agent">
|
||||
|
|
|
@ -19,7 +19,8 @@ angular
|
|||
EndpointProvider,
|
||||
Notifications,
|
||||
Authentication,
|
||||
SettingsService
|
||||
SettingsService,
|
||||
ModalService
|
||||
) {
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
|
@ -113,6 +114,29 @@ angular
|
|||
}
|
||||
}
|
||||
|
||||
$scope.onDeassociateEndpoint = async function () {
|
||||
ModalService.confirmDeassociate((confirmed) => {
|
||||
if (confirmed) {
|
||||
deassociateEndpoint();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function deassociateEndpoint() {
|
||||
var endpoint = $scope.endpoint;
|
||||
|
||||
try {
|
||||
$scope.state.actionInProgress = true;
|
||||
await EndpointService.deassociateEndpoint(endpoint.Id);
|
||||
Notifications.success('Endpoint de-associated', $scope.endpoint.Name);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to de-associate endpoint');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.updateEndpoint = function () {
|
||||
var endpoint = $scope.endpoint;
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
|
|
Loading…
Reference in New Issue