fix(kubernetes): remove unique check from kubernetes stacks [EE-6170] (#10542)

pull/10448/head
Prabhat Khera 2023-10-27 15:41:02 +13:00 committed by GitHub
parent 8ee718f808
commit 26036c05f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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