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 <ericsun@SG1.local>
pull/5966/head^2
sunportainer 2021-11-22 17:23:56 +13:00 committed by GitHub
parent 5f2e3452e4
commit cea634a7aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 3 deletions

View File

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

View File

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

View File

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