diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 6ceab991c..ddb877985 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -2,6 +2,7 @@ package stacks import ( "errors" + "fmt" "net/http" "path" "regexp" @@ -47,15 +48,12 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -133,15 +131,12 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -229,15 +224,12 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 0113e8a41..7e751a59f 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -2,10 +2,10 @@ package stacks import ( "errors" + "fmt" "net/http" "path" "strconv" - "strings" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" @@ -42,15 +42,12 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -132,15 +129,12 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -236,15 +230,12 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.DataStore.Stack().Stacks() + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } - - for _, stack := range stacks { - if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} - } + if !isUnique { + return &httperror.HandlerError{http.StatusConflict, fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), errStackAlreadyExists} } stackID := handler.DataStore.Stack().GetNextIdentifier() diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 6ea263ae8..4eab17a36 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -1,13 +1,17 @@ package stacks import ( + "context" "errors" + "github.com/docker/docker/api/types" "net/http" + "strings" "sync" "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -24,6 +28,7 @@ type Handler struct { requestBouncer *security.RequestBouncer *mux.Router DataStore portainer.DataStore + DockerClientFactory *docker.ClientFactory FileService portainer.FileService GitService portainer.GitService SwarmStackManager portainer.SwarmStackManager @@ -94,3 +99,50 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR return handler.userIsAdminOrEndpointAdmin(user, endpointID) } + +func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) { + stacks, err := handler.DataStore.Stack().Stacks() + if err != nil { + return false, err + } + + for _, stack := range stacks { + if strings.EqualFold(stack.Name, name) && (stackID == 0 || stackID != stack.ID) { + return false, nil + } + } + + dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "") + if err != nil { + return false, err + } + defer dockerClient.Close() + if swarmMode { + services, err := dockerClient.ServiceList(context.Background(), types.ServiceListOptions{}) + if err != nil { + return false, err + } + + for _, service := range services { + serviceNS, ok := service.Spec.Labels["com.docker.stack.namespace"] + if ok && serviceNS == name { + return false, nil + } + } + } + + containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true}) + if err != nil { + return false, err + } + + for _, container := range containers { + containerNS, ok := container.Labels["com.docker.compose.project"] + + if ok && containerNS == name { + return false, nil + } + } + + return true, nil +} diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index 80d1f694e..41389e54a 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -2,6 +2,7 @@ package stacks import ( "errors" + "fmt" "net/http" httperrors "github.com/portainer/portainer/api/http/errors" @@ -45,6 +46,15 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } + isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "") + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} + } + if !isUnique { + errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name) + return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} + } + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} diff --git a/api/http/server.go b/api/http/server.go index 614d0bcea..6551ee89f 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -179,6 +179,7 @@ func (server *Server) Start() error { stackHandler.ComposeStackManager = server.ComposeStackManager stackHandler.KubernetesDeployer = server.KubernetesDeployer stackHandler.GitService = server.GitService + stackHandler.DockerClientFactory = server.DockerClientFactory var statusHandler = status.NewHandler(requestBouncer, server.Status) statusHandler.DataStore = server.DataStore