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
cong meng 2021-08-02 18:08:40 +12:00 committed by GitHub
parent ce31de5e9e
commit 5652bac004
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 1 deletions

View File

@ -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)
}

View File

@ -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",

View File

@ -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' } },

View File

@ -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)

View File

@ -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({

View File

@ -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">

View File

@ -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;