From cea634a7aa75f560a44d627bfc3a42f565bdffb3 Mon Sep 17 00:00:00 2001 From: sunportainer <93502624+sunportainer@users.noreply.github.com> Date: Mon, 22 Nov 2021 17:23:56 +1300 Subject: [PATCH] fix(stack): support removing duplicated stacks EE-1962 (#6068) * fix/EE-1962/cannot-same-stack-name handle multiple names duplicate case Co-authored-by: Eric Sun --- api/bolt/stack/stack.go | 25 ++++++ .../handler/stacks/create_compose_stack.go | 80 ++++++++++++++++++- api/portainer.go | 1 + 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/api/bolt/stack/stack.go b/api/bolt/stack/stack.go index a2af7e1f9..1ff1a3cad 100644 --- a/api/bolt/stack/stack.go +++ b/api/bolt/stack/stack.go @@ -77,6 +77,31 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) { return stack, err } +// Stacks returns an array containing all the stacks with same name +func (service *Service) StacksByName(name string) ([]portainer.Stack, error) { + var stacks = make([]portainer.Stack, 0) + + err := service.connection.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var t portainer.Stack + err := internal.UnmarshalObject(v, &t) + if err != nil { + return err + } + + if t.Name == name { + stacks = append(stacks, t) + } + } + + return nil + }) + + return stacks, err +} + // Stacks returns an array containing all the stacks. func (service *Service) Stacks() ([]portainer.Stack, error) { var stacks = make([]portainer.Stack, 0) diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 4d8c43929..ab039c3e9 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 ( "fmt" + "log" "net/http" "strconv" "time" @@ -14,6 +15,7 @@ import ( "github.com/portainer/portainer/api/filesystem" gittypes "github.com/portainer/portainer/api/git/types" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/stackutils" ) type composeStackFromFileContentPayload struct { @@ -35,6 +37,36 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err } return nil } +func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID, stack *portainer.Stack) error { + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl) + if err != nil { + return err + } + // stop scheduler updates of the stack before removal + if stack.AutoUpdate != nil { + stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler) + } + + err = handler.DataStore.Stack().DeleteStack(stack.ID) + if err != nil { + return err + } + + if resourceControl != nil { + err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) + if err != nil { + log.Printf("[ERROR] [Stack] Unable to remove the associated resource control from the database for stack: [%+v].", stack) + } + } + + if exists, _ := handler.FileService.FileExists(stack.ProjectPath); exists { + err = handler.FileService.RemoveDirectory(stack.ProjectPath) + if err != nil { + log.Printf("Unable to remove stack files from disk for stack: [%+v].", stack) + } + } + return nil +} func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { var payload composeStackFromFileContentPayload @@ -49,8 +81,22 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} } + if !isUnique { - return stackExistsError(payload.Name) + stacks, err := handler.DataStore.Stack().StacksByName(payload.Name) + if err != nil { + return stackExistsError(payload.Name) + } + for _, stack := range stacks { + if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID { + err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} + } + } else { + return stackExistsError(payload.Name) + } + } } stackID := handler.DataStore.Stack().GetNextIdentifier() @@ -154,8 +200,22 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } + if !isUnique { - return stackExistsError(payload.Name) + stacks, err := handler.DataStore.Stack().StacksByName(payload.Name) + if err != nil { + return stackExistsError(payload.Name) + } + for _, stack := range stacks { + if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID { + err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} + } + } else { + return stackExistsError(payload.Name) + } + } } //make sure the webhook ID is unique @@ -283,8 +343,22 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err} } + if !isUnique { - return stackExistsError(payload.Name) + stacks, err := handler.DataStore.Stack().StacksByName(payload.Name) + if err != nil { + return stackExistsError(payload.Name) + } + for _, stack := range stacks { + if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID { + err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} + } + } else { + return stackExistsError(payload.Name) + } + } } stackID := handler.DataStore.Stack().GetNextIdentifier() diff --git a/api/portainer.go b/api/portainer.go index b37e8ab7d..f951866f3 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1379,6 +1379,7 @@ type ( StackService interface { Stack(ID StackID) (*Stack, error) StackByName(name string) (*Stack, error) + StacksByName(name string) ([]Stack, error) Stacks() ([]Stack, error) CreateStack(stack *Stack) error UpdateStack(ID StackID, stack *Stack) error