mirror of https://github.com/portainer/portainer
fix: normalize stack name only for libcompose (#4862)
* fix: normilize stack name only for libcompose * fixpull/4666/head
parent
6d5877ca1c
commit
4cbd231a5f
|
@ -36,6 +36,11 @@ func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
|
||||||
return portainer.ComposeSyntaxMaxVersion
|
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
|
// 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 {
|
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
|
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
|
||||||
|
|
|
@ -5,9 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -18,13 +16,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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 {
|
type composeStackFromFileContentPayload struct {
|
||||||
// Name of the stack
|
// Name of the stack
|
||||||
Name string `example:"myStack" validate:"required"`
|
Name string `example:"myStack" validate:"required"`
|
||||||
|
@ -38,7 +29,7 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return errors.New("Invalid stack name")
|
return errors.New("Invalid stack name")
|
||||||
}
|
}
|
||||||
payload.Name = normalizeStackName(payload.Name)
|
|
||||||
if govalidator.IsNull(payload.StackFileContent) {
|
if govalidator.IsNull(payload.StackFileContent) {
|
||||||
return errors.New("Invalid stack file content")
|
return errors.New("Invalid stack file content")
|
||||||
}
|
}
|
||||||
|
@ -49,9 +40,11 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
var payload composeStackFromFileContentPayload
|
var payload composeStackFromFileContentPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
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)
|
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
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))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||||
if err != nil {
|
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
|
stack.ProjectPath = projectPath
|
||||||
|
|
||||||
|
@ -90,14 +83,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
|
|
||||||
err = handler.deployComposeStack(config)
|
err = handler.deployComposeStack(config)
|
||||||
if err != nil {
|
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
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
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
|
doCleanUp = false
|
||||||
|
@ -129,16 +122,14 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return errors.New("Invalid stack name")
|
return errors.New("Invalid stack name")
|
||||||
}
|
}
|
||||||
payload.Name = normalizeStackName(payload.Name)
|
|
||||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||||
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
|
||||||
}
|
}
|
||||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
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")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +137,12 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
var payload composeStackFromGitRepositoryPayload
|
var payload composeStackFromGitRepositoryPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
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)
|
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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||||
}
|
}
|
||||||
if !isUnique {
|
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)}
|
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +183,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
|
|
||||||
err = handler.cloneGitRepository(gitCloneParams)
|
err = handler.cloneGitRepository(gitCloneParams)
|
||||||
if err != nil {
|
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)
|
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
|
||||||
|
@ -197,14 +193,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
|
|
||||||
err = handler.deployComposeStack(config)
|
err = handler.deployComposeStack(config)
|
||||||
if err != nil {
|
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
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
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
|
doCleanUp = false
|
||||||
|
@ -217,41 +213,43 @@ type composeStackFromFileUploadPayload struct {
|
||||||
Env []portainer.Pair
|
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)
|
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||||
if err != nil {
|
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")
|
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||||
if err != nil {
|
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
|
payload.StackFileContent = composeFileContent
|
||||||
|
|
||||||
var env []portainer.Pair
|
var env []portainer.Pair
|
||||||
err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true)
|
err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Invalid Env parameter")
|
return nil, errors.New("Invalid Env parameter")
|
||||||
}
|
}
|
||||||
payload.Env = env
|
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 {
|
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
payload := &composeStackFromFileUploadPayload{}
|
payload, err := decodeRequestForm(r)
|
||||||
err := payload.Validate(r)
|
|
||||||
if err != nil {
|
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)
|
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
|
||||||
}
|
}
|
||||||
if !isUnique {
|
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)}
|
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))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
|
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
|
||||||
if err != nil {
|
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
|
stack.ProjectPath = projectPath
|
||||||
|
|
||||||
|
@ -284,14 +282,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
|
|
||||||
err = handler.deployComposeStack(config)
|
err = handler.deployComposeStack(config)
|
||||||
if err != nil {
|
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
|
stack.CreatedBy = config.user.Username
|
||||||
|
|
||||||
err = handler.DataStore.Stack().CreateStack(stack)
|
err = handler.DataStore.Stack().CreateStack(stack)
|
||||||
if err != nil {
|
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
|
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) {
|
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
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()
|
dockerhub, err := handler.DataStore.DockerHub().DockerHub()
|
||||||
if err != nil {
|
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()
|
registries, err := handler.DataStore.Registry().Registries()
|
||||||
if err != nil {
|
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)
|
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||||
|
|
||||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||||
if err != nil {
|
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{
|
config := &composeStackDeploymentConfig{
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/portainer/libcompose/config"
|
"github.com/portainer/libcompose/config"
|
||||||
"github.com/portainer/libcompose/docker"
|
"github.com/portainer/libcompose/docker"
|
||||||
|
@ -64,6 +66,14 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
|
||||||
return composeSyntaxMaxVersion
|
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)
|
// Up will deploy a compose stack (equivalent of docker-compose up)
|
||||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
|
||||||
|
|
|
@ -973,6 +973,7 @@ type (
|
||||||
// ComposeStackManager represents a service to manage Compose stacks
|
// ComposeStackManager represents a service to manage Compose stacks
|
||||||
ComposeStackManager interface {
|
ComposeStackManager interface {
|
||||||
ComposeSyntaxMaxVersion() string
|
ComposeSyntaxMaxVersion() string
|
||||||
|
NormalizeStackName(name string) string
|
||||||
Up(stack *Stack, endpoint *Endpoint) error
|
Up(stack *Stack, endpoint *Endpoint) error
|
||||||
Down(stack *Stack, endpoint *Endpoint) error
|
Down(stack *Stack, endpoint *Endpoint) error
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue