mirror of https://github.com/portainer/portainer
				
				
				
			fix(kubernetes): remove unique check from kubernetes stacks [EE-6170] (#10542)
							parent
							
								
									8ee718f808
								
							
						
					
					
						commit
						26036c05f2
					
				|  | @ -153,13 +153,6 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit | |||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to load user information from the database", err) | ||||
| 	} | ||||
| 	isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) | ||||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to check for name collision", err) | ||||
| 	} | ||||
| 	if !isUnique { | ||||
| 		return httperror.Conflict(fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), stackutils.ErrStackAlreadyExists) | ||||
| 	} | ||||
| 
 | ||||
| 	stackPayload := createStackPayloadFromK8sFileContentPayload(payload.StackName, payload.Namespace, payload.StackFileContent, payload.ComposeFormat, payload.FromAppTemplate) | ||||
| 
 | ||||
|  | @ -218,13 +211,6 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr | |||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to load user information from the database", err) | ||||
| 	} | ||||
| 	isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) | ||||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to check for name collision", err) | ||||
| 	} | ||||
| 	if !isUnique { | ||||
| 		return httperror.Conflict(fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), stackutils.ErrStackAlreadyExists) | ||||
| 	} | ||||
| 
 | ||||
| 	//make sure the webhook ID is unique
 | ||||
| 	if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" { | ||||
|  | @ -296,13 +282,6 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit | |||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to load user information from the database", err) | ||||
| 	} | ||||
| 	isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) | ||||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to check for name collision", err) | ||||
| 	} | ||||
| 	if !isUnique { | ||||
| 		return httperror.Conflict(fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), stackutils.ErrStackAlreadyExists) | ||||
| 	} | ||||
| 
 | ||||
| 	stackPayload := createStackPayloadFromK8sUrlPayload(payload.StackName, | ||||
| 		payload.Namespace, | ||||
|  |  | |||
|  | @ -70,6 +70,8 @@ func NewHandler(bouncer security.BouncerService) *Handler { | |||
| 		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete) | ||||
| 	h.Handle("/stacks/{id}/associate", | ||||
| 		bouncer.AdminAccess(httperror.LoggerHandler(h.stackAssociate))).Methods(http.MethodPut) | ||||
| 	h.Handle("/stacks/name/{name}", | ||||
| 		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDeleteKubernetesByName))).Methods(http.MethodDelete) | ||||
| 	h.Handle("/stacks/{id}", | ||||
| 		bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut) | ||||
| 	h.Handle("/stacks/{id}/git", | ||||
|  | @ -163,31 +165,6 @@ func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name | |||
| 	return true, nil | ||||
| } | ||||
| 
 | ||||
| func (handler *Handler) checkUniqueStackNameInKubernetes(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, namespace string) (bool, error) { | ||||
| 	isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 
 | ||||
| 	if !isUniqueStackName { | ||||
| 		// Check if this stack name is really used in the kubernetes.
 | ||||
| 		// Because the stack with this name could be removed via kubectl cli outside and the datastore does not be informed of this action.
 | ||||
| 		if namespace == "" { | ||||
| 			namespace = "default" | ||||
| 		} | ||||
| 
 | ||||
| 		kubeCli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 		isUniqueStackName, err = kubeCli.HasStackName(namespace, name) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
| 	return isUniqueStackName, nil | ||||
| } | ||||
| 
 | ||||
| func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) { | ||||
| 	isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -251,3 +251,137 @@ func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.St | |||
| 
 | ||||
| 	return fmt.Errorf("unsupported stack type: %v", stack.Type) | ||||
| } | ||||
| 
 | ||||
| // @id StackDeleteKubernetesByName
 | ||||
| // @summary Remove Kubernetes stacks by name
 | ||||
| // @description Remove a stack.
 | ||||
| // @description **Access policy**: restricted
 | ||||
| // @tags stacks
 | ||||
| // @security ApiKeyAuth
 | ||||
| // @security jwt
 | ||||
| // @param name path string true "Stack name"
 | ||||
| // @param external query boolean false "Set to true to delete an external stack. Only external Swarm stacks are supported"
 | ||||
| // @param endpointId query int true "Environment identifier"
 | ||||
| // @success 204 "Success"
 | ||||
| // @failure 400 "Invalid request"
 | ||||
| // @failure 403 "Permission denied"
 | ||||
| // @failure 404 "Not found"
 | ||||
| // @failure 500 "Server error"
 | ||||
| // @router /stacks/name/{name} [delete]
 | ||||
| func (handler *Handler) stackDeleteKubernetesByName(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { | ||||
| 	stackName, err := request.RetrieveRouteVariableValue(r, "name") | ||||
| 	if err != nil { | ||||
| 		return httperror.BadRequest("Invalid stack identifier route variable", err) | ||||
| 	} | ||||
| 
 | ||||
| 	log.Debug().Msgf("Trying to delete Kubernetes stack %q", stackName) | ||||
| 
 | ||||
| 	securityContext, err := security.RetrieveRestrictedRequestContext(r) | ||||
| 	if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to retrieve info from request context", err) | ||||
| 	} | ||||
| 
 | ||||
| 	endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true) | ||||
| 	if err != nil { | ||||
| 		return httperror.BadRequest("Invalid query parameter: endpointId", err) | ||||
| 	} | ||||
| 
 | ||||
| 	namespace, err := request.RetrieveQueryParameter(r, "namespace", false) | ||||
| 	if err != nil { | ||||
| 		return httperror.BadRequest("Invalid query parameter: namespace", err) | ||||
| 	} | ||||
| 
 | ||||
| 	stacks, err := handler.DataStore.Stack().StacksByName(stackName) | ||||
| 	if err != nil && !handler.DataStore.IsErrObjectNotFound(err) { | ||||
| 		return httperror.InternalServerError("Unable to check for stack existence inside the database", err) | ||||
| 	} | ||||
| 	if stacks == nil { | ||||
| 		return httperror.InternalServerError("Unable to find a stacks with the specified identifier name the database", err) | ||||
| 	} | ||||
| 
 | ||||
| 	endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) | ||||
| 	if handler.DataStore.IsErrObjectNotFound(err) { | ||||
| 		return httperror.NotFound("Unable to find the endpoint associated to the stack inside the database", err) | ||||
| 	} else if err != nil { | ||||
| 		return httperror.InternalServerError("Unable to find the endpoint associated to the stack inside the database", err) | ||||
| 	} | ||||
| 
 | ||||
| 	log.Debug().Msgf("Trying to delete Kubernetes stack %q for endpoint `%d`", stackName, endpointID) | ||||
| 
 | ||||
| 	// check authorizations on all the stacks one by one
 | ||||
| 	stacksToDelete := make([]portainer.Stack, 0) | ||||
| 	for _, stack := range stacks { | ||||
| 		// only delete stacks for the specified namespace
 | ||||
| 		if stack.Namespace != namespace { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID | ||||
| 		if stack.Type != portainer.KubernetesStack { | ||||
| 			return httperror.BadRequest("Only Kubernetes stacks can be deleted by name", errors.New("Only Kubernetes stacks can be deleted by name")) | ||||
| 		} | ||||
| 
 | ||||
| 		if isOrphaned && !securityContext.IsAdmin { | ||||
| 			return httperror.Forbidden("Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")) | ||||
| 		} | ||||
| 
 | ||||
| 		if !isOrphaned { | ||||
| 			err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint) | ||||
| 			if err != nil { | ||||
| 				return httperror.Forbidden("Permission denied to access endpoint", err) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		canManage, err := handler.userCanManageStacks(securityContext, endpoint) | ||||
| 		if err != nil { | ||||
| 			return httperror.InternalServerError("Unable to verify user authorizations to validate stack deletion", err) | ||||
| 		} | ||||
| 		if !canManage { | ||||
| 			errMsg := "stack deletion is disabled for non-admin users" | ||||
| 			return httperror.Forbidden(errMsg, fmt.Errorf(errMsg)) | ||||
| 		} | ||||
| 
 | ||||
| 		stacksToDelete = append(stacksToDelete, stack) | ||||
| 	} | ||||
| 
 | ||||
| 	log.Debug().Msgf("Trying to delete Kubernetes stacks `%v` for endpoint `%d`", stacksToDelete, endpointID) | ||||
| 
 | ||||
| 	errors := make([]error, 0) | ||||
| 	// Delete all the stacks one by one
 | ||||
| 	for _, stack := range stacksToDelete { | ||||
| 		log.Debug().Msgf("Trying to delete Kubernetes stack id `%d`", stack.ID) | ||||
| 
 | ||||
| 		// stop scheduler updates of the stack before removal
 | ||||
| 		if stack.AutoUpdate != nil { | ||||
| 			deployments.StopAutoupdate(stack.ID, stack.AutoUpdate.JobID, handler.Scheduler) | ||||
| 		} | ||||
| 
 | ||||
| 		err = handler.deleteStack(securityContext.UserID, &stack, endpoint) | ||||
| 		if err != nil { | ||||
| 			log.Err(err).Msgf("Unable to delete Kubernetes stack `%d`", stack.ID) | ||||
| 			errors = append(errors, err) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		err = handler.DataStore.Stack().Delete(stack.ID) | ||||
| 		if err != nil { | ||||
| 			errors = append(errors, err) | ||||
| 			log.Err(err).Msgf("Unable to remove the stack `%d` from the database", stack.ID) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		err = handler.FileService.RemoveDirectory(stack.ProjectPath) | ||||
| 		if err != nil { | ||||
| 			errors = append(errors, err) | ||||
| 			log.Warn().Err(err).Msg("Unable to remove stack files from disk") | ||||
| 		} | ||||
| 
 | ||||
| 		log.Debug().Msgf("Kubernetes stack `%d` deleted", stack.ID) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(errors) > 0 { | ||||
| 		return httperror.InternalServerError("Unable to delete some Kubernetes stack(s). Check Portainer logs for more details", nil) | ||||
| 	} | ||||
| 
 | ||||
| 	return response.Empty(w) | ||||
| } | ||||
|  |  | |||
|  | @ -61,13 +61,10 @@ class KubernetesApplicationsController { | |||
|         if (isAppFormCreated) { | ||||
|           const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app)); | ||||
|           await Promise.all(promises); | ||||
|         } else { | ||||
|           const application = stack.Applications.find((x) => x.StackId !== null); | ||||
|           if (application && application.StackId) { | ||||
|             await this.StackService.remove({ Id: application.StackId }, false, this.endpoint.Id); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         await this.StackService.removeKubernetesStacksByName(stack.Name, stack.ResourcePool, false, this.endpoint.Id); | ||||
| 
 | ||||
|         this.Notifications.success('Stack successfully removed', stack.Name); | ||||
|         _.remove(this.state.stacks, { Name: stack.Name }); | ||||
|       } catch (err) { | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import angular from 'angular'; | ||||
| 
 | ||||
| angular.module('portainer.app').factory('Stack', StackFactory); | ||||
| angular.module('portainer.app').factory('StackByName', StackByNameFactory); | ||||
| 
 | ||||
| /* @ngInject */ | ||||
| function StackFactory($resource, API_ENDPOINT_STACKS) { | ||||
|  | @ -23,3 +24,13 @@ function StackFactory($resource, API_ENDPOINT_STACKS) { | |||
|     } | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function StackByNameFactory($resource, API_ENDPOINT_STACKS) { | ||||
|   return $resource( | ||||
|     API_ENDPOINT_STACKS + '/name/:name', | ||||
|     {}, | ||||
|     { | ||||
|       remove: { method: 'DELETE', params: { name: '@name', external: '@external', endpointId: '@endpointId', namespace: '@namespace' } }, | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -6,12 +6,13 @@ angular.module('portainer.app').factory('StackService', [ | |||
|   '$q', | ||||
|   '$async', | ||||
|   'Stack', | ||||
|   'StackByName', | ||||
|   'FileUploadService', | ||||
|   'StackHelper', | ||||
|   'ServiceService', | ||||
|   'ContainerService', | ||||
|   'SwarmService', | ||||
|   function StackServiceFactory($q, $async, Stack, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService) { | ||||
|   function StackServiceFactory($q, $async, Stack, StackByName, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService) { | ||||
|     'use strict'; | ||||
|     var service = { | ||||
|       updateGit, | ||||
|  | @ -221,6 +222,19 @@ angular.module('portainer.app').factory('StackService', [ | |||
|       return deferred.promise; | ||||
|     }; | ||||
| 
 | ||||
|     service.removeKubernetesStacksByName = function (name, namespace, external, endpointId) { | ||||
|       var deferred = $q.defer(); | ||||
|       StackByName.remove({ name: name, external: external, endpointId: endpointId, namespace: namespace }) | ||||
|         .$promise.then(function success() { | ||||
|           deferred.resolve(); | ||||
|         }) | ||||
|         .catch(function error(err) { | ||||
|           deferred.reject({ msg: 'Unable to remove the stack', err: err }); | ||||
|         }); | ||||
| 
 | ||||
|       return deferred.promise; | ||||
|     }; | ||||
| 
 | ||||
|     service.associate = function (stack, endpointId, orphanedRunning) { | ||||
|       var deferred = $q.defer(); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Prabhat Khera
						Prabhat Khera