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 {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load user information from the database", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load user information from the database", err)
|
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
|
//make sure the webhook ID is unique
|
||||||
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
|
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
|
||||||
|
@ -296,13 +282,6 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httperror.InternalServerError("Unable to load user information from the database", err)
|
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,
|
stackPayload := createStackPayloadFromK8sUrlPayload(payload.StackName,
|
||||||
payload.Namespace,
|
payload.Namespace,
|
||||||
|
|
|
@ -70,6 +70,8 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/stacks/{id}/associate",
|
h.Handle("/stacks/{id}/associate",
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.stackAssociate))).Methods(http.MethodPut)
|
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}",
|
h.Handle("/stacks/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/stacks/{id}/git",
|
h.Handle("/stacks/{id}/git",
|
||||||
|
@ -163,31 +165,6 @@ func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name
|
||||||
return true, nil
|
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) {
|
func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
|
||||||
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
|
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
|
||||||
if err != nil {
|
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)
|
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) {
|
if (isAppFormCreated) {
|
||||||
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app));
|
||||||
await Promise.all(promises);
|
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);
|
this.Notifications.success('Stack successfully removed', stack.Name);
|
||||||
_.remove(this.state.stacks, { Name: stack.Name });
|
_.remove(this.state.stacks, { Name: stack.Name });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
angular.module('portainer.app').factory('Stack', StackFactory);
|
angular.module('portainer.app').factory('Stack', StackFactory);
|
||||||
|
angular.module('portainer.app').factory('StackByName', StackByNameFactory);
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
function StackFactory($resource, API_ENDPOINT_STACKS) {
|
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',
|
'$q',
|
||||||
'$async',
|
'$async',
|
||||||
'Stack',
|
'Stack',
|
||||||
|
'StackByName',
|
||||||
'FileUploadService',
|
'FileUploadService',
|
||||||
'StackHelper',
|
'StackHelper',
|
||||||
'ServiceService',
|
'ServiceService',
|
||||||
'ContainerService',
|
'ContainerService',
|
||||||
'SwarmService',
|
'SwarmService',
|
||||||
function StackServiceFactory($q, $async, Stack, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService) {
|
function StackServiceFactory($q, $async, Stack, StackByName, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {
|
var service = {
|
||||||
updateGit,
|
updateGit,
|
||||||
|
@ -221,6 +222,19 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
return deferred.promise;
|
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) {
|
service.associate = function (stack, endpointId, orphanedRunning) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue