From 4cbd231a5fee5f9f29ee190e75b502b43e8a7028 Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Mon, 15 Mar 2021 08:08:31 +1300 Subject: [PATCH] fix: normalize stack name only for libcompose (#4862) * fix: normilize stack name only for libcompose * fix --- api/exec/compose_wrapper.go | 5 ++ .../handler/stacks/create_compose_stack.go | 78 +++++++++---------- api/libcompose/compose_stack.go | 10 +++ api/portainer.go | 1 + 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/api/exec/compose_wrapper.go b/api/exec/compose_wrapper.go index 5f91795fd..918411685 100644 --- a/api/exec/compose_wrapper.go +++ b/api/exec/compose_wrapper.go @@ -36,6 +36,11 @@ func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string { return portainer.ComposeSyntaxMaxVersion } +// NormalizeStackName returns a new stack name with unsupported characters replaced +func (w *ComposeWrapper) NormalizeStackName(name string) string { + return name +} + // Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error { _, err := w.command([]string{"up", "-d"}, stack, endpoint) diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 0dc88c922..15658fee5 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -5,9 +5,7 @@ import ( "fmt" "net/http" "path" - "regexp" "strconv" - "strings" "time" "github.com/asaskevich/govalidator" @@ -18,13 +16,6 @@ import ( "github.com/portainer/portainer/api/http/security" ) -// this is coming from libcompose -// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120 -func normalizeStackName(name string) string { - r := regexp.MustCompile("[^a-z0-9]+") - return r.ReplaceAllString(strings.ToLower(name), "") -} - type composeStackFromFileContentPayload struct { // Name of the stack Name string `example:"myStack" validate:"required"` @@ -38,7 +29,7 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err if govalidator.IsNull(payload.Name) { return errors.New("Invalid stack name") } - payload.Name = normalizeStackName(payload.Name) + if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") } @@ -49,9 +40,11 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, var payload composeStackFromFileContentPayload err := request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} } + payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name) + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err} @@ -76,7 +69,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, stackFolder := strconv.Itoa(int(stack.ID)) projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Compose file on disk", Err: err} } stack.ProjectPath = projectPath @@ -90,14 +83,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, err = handler.deployComposeStack(config) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err} } stack.CreatedBy = config.user.Username err = handler.DataStore.Stack().CreateStack(stack) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err} } doCleanUp = false @@ -129,16 +122,14 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e if govalidator.IsNull(payload.Name) { return errors.New("Invalid stack name") } - payload.Name = normalizeStackName(payload.Name) + if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { return errors.New("Invalid repository URL. Must correspond to a valid URL format") } if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } - if govalidator.IsNull(payload.ComposeFilePathInRepository) { - payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName - } + return nil } @@ -146,7 +137,12 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite var payload composeStackFromGitRepositoryPayload err := request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} + } + + payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name) + if payload.ComposeFilePathInRepository == "" { + payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) @@ -154,7 +150,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite 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", payload.Name) + errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name) return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} } @@ -187,7 +183,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite err = handler.cloneGitRepository(gitCloneParams) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err} } config, configErr := handler.createComposeDeployConfig(r, stack, endpoint) @@ -197,14 +193,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite err = handler.deployComposeStack(config) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err} } stack.CreatedBy = config.user.Username err = handler.DataStore.Stack().CreateStack(stack) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err} } doCleanUp = false @@ -217,41 +213,43 @@ type composeStackFromFileUploadPayload struct { Env []portainer.Pair } -func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) error { +func decodeRequestForm(r *http.Request) (*composeStackFromFileUploadPayload, error) { + payload := &composeStackFromFileUploadPayload{} name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return errors.New("Invalid stack name") + return nil, errors.New("Invalid stack name") } - payload.Name = normalizeStackName(name) + payload.Name = name composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + return nil, errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } payload.StackFileContent = composeFileContent var env []portainer.Pair err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true) if err != nil { - return errors.New("Invalid Env parameter") + return nil, errors.New("Invalid Env parameter") } payload.Env = env - return nil + return payload, nil } func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError { - payload := &composeStackFromFileUploadPayload{} - err := payload.Validate(r) + payload, err := decodeRequestForm(r) if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} } + payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name) + isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false) 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", payload.Name) + errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name) return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)} } @@ -270,7 +268,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, stackFolder := strconv.Itoa(int(stack.ID)) projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Compose file on disk", Err: err} } stack.ProjectPath = projectPath @@ -284,14 +282,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, err = handler.deployComposeStack(config) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err} } stack.CreatedBy = config.user.Username err = handler.DataStore.Stack().CreateStack(stack) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err} } doCleanUp = false @@ -310,23 +308,23 @@ type composeStackDeploymentConfig struct { func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) { securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err} } dockerhub, err := handler.DataStore.DockerHub().DockerHub() if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err} + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve DockerHub details from the database", Err: err} } registries, err := handler.DataStore.Registry().Registries() if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve registries from the database", Err: err} } filteredRegistries := security.FilterRegistries(registries, securityContext) user, err := handler.DataStore.User().User(securityContext.UserID) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err} } config := &composeStackDeploymentConfig{ diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index 4ac6ebdb9..41da7239d 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -5,6 +5,8 @@ import ( "fmt" "path" "path/filepath" + "regexp" + "strings" "github.com/portainer/libcompose/config" "github.com/portainer/libcompose/docker" @@ -64,6 +66,14 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string { return composeSyntaxMaxVersion } +// NormalizeStackName returns a new stack name with unsupported characters replaced +func (manager *ComposeStackManager) NormalizeStackName(name string) string { + // this is coming from libcompose + // https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120 + r := regexp.MustCompile("[^a-z0-9]+") + return r.ReplaceAllString(strings.ToLower(name), "") +} + // Up will deploy a compose stack (equivalent of docker-compose up) func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error { diff --git a/api/portainer.go b/api/portainer.go index 12e8a7a8e..0921f723b 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -973,6 +973,7 @@ type ( // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { ComposeSyntaxMaxVersion() string + NormalizeStackName(name string) string Up(stack *Stack, endpoint *Endpoint) error Down(stack *Stack, endpoint *Endpoint) error }