diff --git a/api/http/handler/edgestacks/edgestack_status_delete.go b/api/http/handler/edgestacks/edgestack_status_delete.go new file mode 100644 index 000000000..8adc245a0 --- /dev/null +++ b/api/http/handler/edgestacks/edgestack_status_delete.go @@ -0,0 +1,64 @@ +package edgestacks + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/middlewares" +) + +func (handler *Handler) handlerDBErr(err error, msg string) *httperror.HandlerError { + httpErr := &httperror.HandlerError{http.StatusInternalServerError, msg, err} + + if handler.DataStore.IsErrObjectNotFound(err) { + httpErr.StatusCode = http.StatusNotFound + } + + return httpErr +} + +// @id EdgeStackStatusDelete +// @summary Delete an EdgeStack status +// @description Authorized only if the request is done by an Edge Environment(Endpoint) +// @tags edge_stacks +// @produce json +// @param id path string true "EdgeStack Id" +// @success 200 {object} portainer.EdgeStack +// @failure 500 +// @failure 400 +// @failure 404 +// @failure 403 +// @router /edge_stacks/{id}/status/{endpoint_id} [delete] +func (handler *Handler) edgeStackStatusDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} + } + + endpoint, err := middlewares.FetchEndpoint(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a valid endpoint from the handler context", err} + } + + err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err} + } + + stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) + if err != nil { + return handler.handlerDBErr(err, "Unable to find a stack with the specified identifier inside the database") + } + + delete(stack.Status, endpoint.ID) + + err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err} + } + + return response.JSON(w, stack) +} diff --git a/api/http/handler/edgestacks/handler.go b/api/http/handler/edgestacks/handler.go index e51c3dc94..ad4a7d17b 100644 --- a/api/http/handler/edgestacks/handler.go +++ b/api/http/handler/edgestacks/handler.go @@ -10,6 +10,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/security" ) @@ -24,10 +25,11 @@ type Handler struct { } // NewHandler creates a handler to manage environment(endpoint) group operations. -func NewHandler(bouncer *security.RequestBouncer) *Handler { +func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore) *Handler { h := &Handler{ Router: mux.NewRouter(), requestBouncer: bouncer, + DataStore: dataStore, } h.Handle("/edge_stacks", bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackCreate)))).Methods(http.MethodPost) @@ -43,6 +45,12 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdminAccess(bouncer.EdgeComputeOperation(httperror.LoggerHandler(h.edgeStackFile)))).Methods(http.MethodGet) h.Handle("/edge_stacks/{id}/status", bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut) + + edgeStackStatusRouter := h.NewRoute().Subrouter() + edgeStackStatusRouter.Use(middlewares.WithEndpoint(h.DataStore.Endpoint(), "endpoint_id")) + + edgeStackStatusRouter.PathPrefix("/edge_stacks/{id}/status/{endpoint_id}").Handler(bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusDelete))).Methods(http.MethodDelete) + return h } diff --git a/api/http/server.go b/api/http/server.go index 15690f2b7..3cabe0b25 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -139,8 +139,7 @@ func (server *Server) Start() error { edgeJobsHandler.FileService = server.FileService edgeJobsHandler.ReverseTunnelService = server.ReverseTunnelService - var edgeStacksHandler = edgestacks.NewHandler(requestBouncer) - edgeStacksHandler.DataStore = server.DataStore + var edgeStacksHandler = edgestacks.NewHandler(requestBouncer, server.DataStore) edgeStacksHandler.FileService = server.FileService edgeStacksHandler.GitService = server.GitService edgeStacksHandler.KubernetesDeployer = server.KubernetesDeployer diff --git a/app/edge/components/group-form/groupFormController.js b/app/edge/components/group-form/groupFormController.js index 7ec10f1bd..8b6b27fc8 100644 --- a/app/edge/components/group-form/groupFormController.js +++ b/app/edge/components/group-form/groupFormController.js @@ -1,4 +1,5 @@ import _ from 'lodash-es'; +import { confirmAsync } from '@/portainer/services/modal.service/confirm'; export class EdgeGroupFormController { /* @ngInject */ @@ -17,6 +18,7 @@ export class EdgeGroupFormController { }; this.associateEndpoint = this.associateEndpoint.bind(this); + this.dissociateEndpointAsync = this.dissociateEndpointAsync.bind(this); this.dissociateEndpoint = this.dissociateEndpoint.bind(this); this.getDynamicEndpointsAsync = this.getDynamicEndpointsAsync.bind(this); this.getDynamicEndpoints = this.getDynamicEndpoints.bind(this); @@ -39,6 +41,29 @@ export class EdgeGroupFormController { } dissociateEndpoint(endpoint) { + return this.$async(this.dissociateEndpointAsync, endpoint); + } + + async dissociateEndpointAsync(endpoint) { + const confirmed = await confirmAsync({ + title: 'Confirm action', + message: 'Removing the environment from this group will remove its corresponding edge stacks', + buttons: { + cancel: { + label: 'Cancel', + className: 'btn-default', + }, + confirm: { + label: 'Confirm', + className: 'btn-primary', + }, + }, + }); + + if (!confirmed) { + return; + } + this.model.Endpoints = _.filter(this.model.Endpoints, (id) => id !== endpoint.Id); } diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 9202352d3..328f031a8 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -5,6 +5,8 @@ import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '@/portainer/components/endpointSecurity/porEndpointSecurityModel'; import EndpointHelper from '@/portainer/helpers/endpointHelper'; import { getAMTInfo } from 'Portainer/hostmanagement/open-amt/open-amt.service'; +import { confirmAsync } from '@/portainer/services/modal.service/confirm'; +import { isEdgeEnvironment } from '@/portainer/environments/utils'; angular.module('portainer.app').controller('EndpointController', EndpointController); @@ -105,7 +107,7 @@ function EndpointController( }); } - $scope.updateEndpoint = function () { + $scope.updateEndpoint = async function () { var endpoint = $scope.endpoint; var securityData = $scope.formValues.SecurityFormData; var TLS = securityData.TLS; @@ -113,6 +115,27 @@ function EndpointController( var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only'); var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only'); + if (isEdgeEnvironment(endpoint.Type) && _.difference($scope.initialTagIds, endpoint.TagIds).length > 0) { + let confirmed = await confirmAsync({ + title: 'Confirm action', + message: 'Removing tags from this environment will remove the corresponding edge stacks when dynamic grouping is being used', + buttons: { + cancel: { + label: 'Cancel', + className: 'btn-default', + }, + confirm: { + label: 'Confirm', + className: 'btn-primary', + }, + }, + }); + + if (!confirmed) { + return; + } + } + var payload = { Name: endpoint.Name, PublicURL: endpoint.PublicURL, @@ -229,6 +252,7 @@ function EndpointController( } $scope.endpoint = endpoint; + $scope.initialTagIds = endpoint.TagIds.slice(); $scope.groups = groups; $scope.availableTags = tags;