diff --git a/api/bolt/migrator/migrate_dbversion26.go b/api/bolt/migrator/migrate_dbversion26.go
new file mode 100644
index 000000000..f894db435
--- /dev/null
+++ b/api/bolt/migrator/migrate_dbversion26.go
@@ -0,0 +1,39 @@
+package migrator
+
+import (
+ "fmt"
+
+ portainer "github.com/portainer/portainer/api"
+)
+
+func (m *Migrator) updateStackResourceControlToDB27() error {
+ resourceControls, err := m.resourceControlService.ResourceControls()
+ if err != nil {
+ return err
+ }
+
+ for _, resource := range resourceControls {
+ if resource.Type != portainer.StackResourceControl {
+ continue
+ }
+
+ stackName := resource.ResourceID
+ if err != nil {
+ return err
+ }
+
+ stack, err := m.stackService.StackByName(stackName)
+ if err != nil {
+ return err
+ }
+
+ resource.ResourceID = fmt.Sprintf("%d_%s", stack.EndpointID, stack.Name)
+
+ err = m.resourceControlService.UpdateResourceControl(resource.ID, &resource)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go
index 468fe0aa7..e366bd3df 100644
--- a/api/bolt/migrator/migrator.go
+++ b/api/bolt/migrator/migrator.go
@@ -2,7 +2,7 @@ package migrator
import (
"github.com/boltdb/bolt"
- "github.com/portainer/portainer/api"
+ portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
@@ -350,5 +350,13 @@ func (m *Migrator) Migrate() error {
}
}
+ // Portainer 2.2.0
+ if m.currentDBVersion < 27 {
+ err := m.updateStackResourceControlToDB27()
+ if err != nil {
+ return err
+ }
+ }
+
return m.versionService.StoreDBVersion(portainer.DBVersion)
}
diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go
index bd0a5d08e..0dc88c922 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 (
"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()
diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go
index b5333650b..e34df227e 100644
--- a/api/http/handler/stacks/create_swarm_stack.go
+++ b/api/http/handler/stacks/create_swarm_stack.go
@@ -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()
diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go
index caa537e2f..f61952515 100644
--- a/api/http/handler/stacks/handler.go
+++ b/api/http/handler/stacks/handler.go
@@ -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
+}
diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go
index 4b9ecd6aa..d3ffe2b53 100644
--- a/api/http/handler/stacks/stack_create.go
+++ b/api/http/handler/stacks/stack_create.go
@@ -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)
diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go
index 53c72fd9e..dd33d38cc 100644
--- a/api/http/handler/stacks/stack_delete.go
+++ b/api/http/handler/stacks/stack_delete.go
@@ -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}
}
diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go
index c7beac1a7..fc11cecd0 100644
--- a/api/http/handler/stacks/stack_file.go
+++ b/api/http/handler/stacks/stack_file.go
@@ -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}
}
diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go
index 3503bdba4..2891279cc 100644
--- a/api/http/handler/stacks/stack_inspect.go
+++ b/api/http/handler/stacks/stack_inspect.go
@@ -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}
}
diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go
index 5e1033502..9f13ef1f5 100644
--- a/api/http/handler/stacks/stack_migrate.go
+++ b/api/http/handler/stacks/stack_migrate.go
@@ -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
diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go
index 6b6a693e7..0a915600b 100644
--- a/api/http/handler/stacks/stack_start.go
+++ b/api/http/handler/stacks/stack_start.go
@@ -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}
}
diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go
index 44cf97bda..7dabfb265 100644
--- a/api/http/handler/stacks/stack_stop.go
+++ b/api/http/handler/stacks/stack_stop.go
@@ -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}
}
diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go
index 8b7796c6d..ea012382a 100644
--- a/api/http/handler/stacks/stack_update.go
+++ b/api/http/handler/stacks/stack_update.go
@@ -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}
}
diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go
index fda598cce..385a3c10b 100644
--- a/api/http/proxy/factory/docker/access_control.go
+++ b/api/http/proxy/factory/docker/access_control.go
@@ -5,10 +5,12 @@ import (
"net/http"
"strings"
+ "github.com/portainer/portainer/api/internal/stackutils"
+
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
- "github.com/portainer/portainer/api"
+ portainer "github.com/portainer/portainer/api"
)
const (
@@ -117,17 +119,17 @@ func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resour
switch resourceType {
case portainer.ContainerResourceControl:
- return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromContainerLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
case portainer.NetworkResourceControl:
- return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromNetworkLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
case portainer.VolumeResourceControl:
- return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromVolumeLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
case portainer.ServiceResourceControl:
- return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromServiceLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
case portainer.ConfigResourceControl:
- return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromConfigLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
case portainer.SecretResourceControl:
- return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls)
+ return getInheritedResourceControlFromSecretLabels(client, transport.endpoint.ID, resourceIdentifier, resourceControls)
}
return nil, nil
@@ -273,8 +275,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
- inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
- resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls)
+ stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
+ stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
+ resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
@@ -282,8 +285,9 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
- inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
- resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls)
+ stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
+ stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
+ resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
@@ -296,6 +300,20 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
return nil, nil
}
+func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string {
+ if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != "" {
+ stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName]
+ return stackutils.ResourceControlID(endpointID, stackName)
+ }
+
+ if resourceLabelsObject[resourceLabelForDockerComposeStackName] != "" {
+ stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName]
+ return stackutils.ResourceControlID(endpointID, stackName)
+ }
+
+ return ""
+}
+
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
if object["Portainer"] == nil {
object["Portainer"] = make(map[string]interface{})
diff --git a/api/http/proxy/factory/docker/configs.go b/api/http/proxy/factory/docker/configs.go
index 0fee44bbc..b64955345 100644
--- a/api/http/proxy/factory/docker/configs.go
+++ b/api/http/proxy/factory/docker/configs.go
@@ -15,15 +15,15 @@ const (
configObjectIdentifier = "ID"
)
-func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, endpointID portainer.EndpointID, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID)
if err != nil {
return nil, err
}
- swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(config.Spec.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go
index dcce9c725..8ac49ce3c 100644
--- a/api/http/proxy/factory/docker/containers.go
+++ b/api/http/proxy/factory/docker/containers.go
@@ -19,7 +19,7 @@ const (
containerObjectIdentifier = "Id"
)
-func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
if err != nil {
return nil, err
@@ -33,14 +33,9 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client,
}
}
- swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
- }
-
- composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
- if composeStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(container.Config.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/proxy/factory/docker/networks.go b/api/http/proxy/factory/docker/networks.go
index 4c3c06d48..8236f42d4 100644
--- a/api/http/proxy/factory/docker/networks.go
+++ b/api/http/proxy/factory/docker/networks.go
@@ -4,11 +4,12 @@ import (
"context"
"net/http"
+ portainer "github.com/portainer/portainer/api"
+
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
- "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
)
@@ -18,15 +19,15 @@ const (
networkObjectName = "Name"
)
-func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, endpointID portainer.EndpointID, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{})
if err != nil {
return nil, err
}
- swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(network.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/proxy/factory/docker/secrets.go b/api/http/proxy/factory/docker/secrets.go
index b57627d8d..40ca1c352 100644
--- a/api/http/proxy/factory/docker/secrets.go
+++ b/api/http/proxy/factory/docker/secrets.go
@@ -15,15 +15,15 @@ const (
secretObjectIdentifier = "ID"
)
-func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, endpointID portainer.EndpointID, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID)
if err != nil {
return nil, err
}
- swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(secret.Spec.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go
index 5453f8ed8..029c16b66 100644
--- a/api/http/proxy/factory/docker/services.go
+++ b/api/http/proxy/factory/docker/services.go
@@ -20,15 +20,15 @@ const (
serviceObjectIdentifier = "ID"
)
-func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
if err != nil {
return nil, err
}
- swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(service.Spec.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go
index 0d9705f82..fa3da18a0 100644
--- a/api/http/proxy/factory/docker/volumes.go
+++ b/api/http/proxy/factory/docker/volumes.go
@@ -8,7 +8,7 @@ import (
"github.com/docker/docker/client"
- "github.com/portainer/portainer/api"
+ portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@@ -18,15 +18,15 @@ const (
volumeObjectIdentifier = "ID"
)
-func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
+func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, endpointID portainer.EndpointID, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
volume, err := dockerClient.VolumeInspect(context.Background(), volumeID)
if err != nil {
return nil, err
}
- swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName]
- if swarmStackName != "" {
- return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
+ stackResourceID := getStackResourceIDFromLabels(volume.Labels, endpointID)
+ if stackResourceID != "" {
+ return authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls), nil
}
return nil, nil
diff --git a/api/http/server.go b/api/http/server.go
index 35571d736..ad7826087 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -157,6 +157,7 @@ func (server *Server) Start() error {
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore
+ stackHandler.DockerClientFactory = server.DockerClientFactory
stackHandler.FileService = server.FileService
stackHandler.SwarmStackManager = server.SwarmStackManager
stackHandler.ComposeStackManager = server.ComposeStackManager
diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go
index eab0a3b16..4f533bffa 100644
--- a/api/internal/authorization/access_control.go
+++ b/api/internal/authorization/access_control.go
@@ -3,7 +3,8 @@ package authorization
import (
"strconv"
- "github.com/portainer/portainer/api"
+ portainer "github.com/portainer/portainer/api"
+ "github.com/portainer/portainer/api/internal/stackutils"
)
// NewAdministratorsOnlyResourceControl will create a new administrators only resource control associated to the resource specified by the
@@ -110,7 +111,7 @@ func NewRestrictedResourceControl(resourceIdentifier string, resourceType portai
func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack {
for idx, stack := range stacks {
- resourceControl := GetResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl, resourceControls)
+ resourceControl := GetResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
stacks[idx].ResourceControl = resourceControl
}
diff --git a/api/internal/stackutils/stackutils.go b/api/internal/stackutils/stackutils.go
new file mode 100644
index 000000000..5b1e9bf43
--- /dev/null
+++ b/api/internal/stackutils/stackutils.go
@@ -0,0 +1,12 @@
+package stackutils
+
+import (
+ "fmt"
+
+ portainer "github.com/portainer/portainer/api"
+)
+
+// ResourceControlID returns the stack resource control id
+func ResourceControlID(endpointID portainer.EndpointID, name string) string {
+ return fmt.Sprintf("%d_%s", endpointID, name)
+}
diff --git a/api/portainer.go b/api/portainer.go
index c25838a99..d78a341f6 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -1312,7 +1312,7 @@ const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.2.0"
// DBVersion is the version number of the Portainer database
- DBVersion = 26
+ DBVersion = 27
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html
index 20a1e47cd..27d86e026 100644
--- a/app/portainer/views/stacks/edit/stack.html
+++ b/app/portainer/views/stacks/edit/stack.html
@@ -225,5 +225,6 @@
-
+
+