diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 01e4f63c5..608947ad6 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -106,7 +106,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err} } - isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0) + isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } @@ -177,7 +177,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err} } - isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0) + isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } @@ -291,7 +291,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err} } - isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0) + isUnique, err := handler.checkUniqueStackNameInKubernetes(endpoint, payload.StackName, 0, payload.Namespace) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index d0043d49b..7602bb505 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -16,6 +16,7 @@ import ( "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" + "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/scheduler" "github.com/portainer/portainer/api/stacks" ) @@ -34,15 +35,16 @@ type Handler struct { stackDeletionMutex *sync.Mutex requestBouncer *security.RequestBouncer *mux.Router - DataStore dataservices.DataStore - DockerClientFactory *docker.ClientFactory - FileService portainer.FileService - GitService portainer.GitService - SwarmStackManager portainer.SwarmStackManager - ComposeStackManager portainer.ComposeStackManager - KubernetesDeployer portainer.KubernetesDeployer - Scheduler *scheduler.Scheduler - StackDeployer stacks.StackDeployer + DataStore dataservices.DataStore + DockerClientFactory *docker.ClientFactory + FileService portainer.FileService + GitService portainer.GitService + SwarmStackManager portainer.SwarmStackManager + ComposeStackManager portainer.ComposeStackManager + KubernetesDeployer portainer.KubernetesDeployer + KubernetesClientFactory *cli.ClientFactory + Scheduler *scheduler.Scheduler + StackDeployer stacks.StackDeployer } func stackExistsError(name string) *httperror.HandlerError { @@ -148,6 +150,31 @@ 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 { diff --git a/api/http/server.go b/api/http/server.go index 0ed3667f6..b8f45fe68 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -221,6 +221,7 @@ func (server *Server) Start() error { stackHandler.DataStore = server.DataStore stackHandler.DockerClientFactory = server.DockerClientFactory stackHandler.FileService = server.FileService + stackHandler.KubernetesClientFactory = server.KubernetesClientFactory stackHandler.KubernetesDeployer = server.KubernetesDeployer stackHandler.GitService = server.GitService stackHandler.Scheduler = server.Scheduler diff --git a/api/kubernetes/cli/deploment.go b/api/kubernetes/cli/deploment.go new file mode 100644 index 000000000..ce0b72c3c --- /dev/null +++ b/api/kubernetes/cli/deploment.go @@ -0,0 +1,22 @@ +package cli + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" +) + +// HasStackName checks whether the given name is used in the given namespace. +func (kcl *KubeClient) HasStackName(namespace string, stackName string) (bool, error) { + querySet := labels.Set{"io.portainer.kubernetes.application.stack": stackName} + listOpts := metav1.ListOptions{LabelSelector: labels.SelectorFromSet(querySet).String()} + list, err := kcl.cli.AppsV1().Deployments(namespace).List(context.TODO(), listOpts) + if err != nil { + return false, err + } + if len(list.Items) > 0 { + return false, nil + } + return true, nil +} diff --git a/api/portainer.go b/api/portainer.go index 1c06b8086..9807896be 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1262,6 +1262,7 @@ type ( GetServiceAccountBearerToken(userID int) (string, error) CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error) + HasStackName(namespace string, stackName string) (bool, error) NamespaceAccessPoliciesDeleteNamespace(namespace string) error GetNodesLimits() (K8sNodesLimits, error) GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)