1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

feat(stacks): scope stack names to endpoint (#4520)

* refactor(stack): create unique name function

* refactor(stack): change stack resource control id

* feat(stacks): validate stack unique name in endpoint

* feat(stacks): prevent name collision with external stacks

* refactor(stacks): move resource id util

* refactor(stacks): supply resource id util with name and endpoint

* fix(docker): calculate swarm resource id

* feat(stack): prevent migration if stack name already exist

* feat(authorization): use stackutils
This commit is contained in:
Chaim Lev-Ari 2021-02-23 22:18:05 +02:00 committed by GitHub
parent a62e0496de
commit 86ad1c6af1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 245 additions and 99 deletions

View file

@ -2,6 +2,7 @@ package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"regexp"
@ -51,15 +52,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@ -150,15 +149,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@ -249,15 +246,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View file

@ -2,10 +2,10 @@ package stacks
import (
"errors"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
@ -47,15 +47,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@ -150,15 +148,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@ -257,15 +253,13 @@ 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 {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View file

@ -1,13 +1,17 @@
package stacks
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "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
@ -103,3 +108,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) && stack.EndpointID == endpoint.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
}

View file

@ -15,6 +15,7 @@ import (
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/stackutils"
)
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
@ -208,9 +209,9 @@ func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *port
}
if isAdmin {
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
} else {
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
resourceControl = authorization.NewPrivateResourceControl(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, userID)
}
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)

View file

@ -12,6 +12,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackDelete
@ -82,7 +83,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View file

@ -11,6 +11,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type stackFileResponse struct {
@ -57,7 +58,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View file

@ -10,6 +10,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackInspect
@ -56,7 +57,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View file

@ -2,6 +2,7 @@ package stacks
import (
"errors"
"fmt"
"net/http"
httperror "github.com/portainer/libhttp/error"
@ -11,6 +12,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type stackMigratePayload struct {
@ -76,7 +78,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
@ -122,6 +124,16 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
stack.Name = payload.Name
}
isUnique, err := handler.checkUniqueName(targetEndpoint, 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 on endpoint '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
}
migrationError := handler.migrateStack(r, stack, targetEndpoint)
if migrationError != nil {
return migrationError

View file

@ -2,11 +2,13 @@ package stacks
import (
"errors"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@ -57,7 +59,16 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
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(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View file

@ -11,6 +11,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
// @id StackStop
@ -56,7 +57,7 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

View file

@ -14,6 +14,7 @@ import (
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type updateComposeStackPayload struct {
@ -99,7 +100,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}