From 86ad1c6af162c2b6e2905a56633001f8b450c1ac Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 23 Feb 2021 22:18:05 +0200 Subject: [PATCH] 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 --- api/bolt/migrator/migrate_dbversion26.go | 39 ++++++++++++++ api/bolt/migrator/migrator.go | 10 +++- .../handler/stacks/create_compose_stack.go | 37 ++++++------- api/http/handler/stacks/create_swarm_stack.go | 38 ++++++-------- api/http/handler/stacks/handler.go | 52 +++++++++++++++++++ api/http/handler/stacks/stack_create.go | 5 +- api/http/handler/stacks/stack_delete.go | 3 +- api/http/handler/stacks/stack_file.go | 3 +- api/http/handler/stacks/stack_inspect.go | 3 +- api/http/handler/stacks/stack_migrate.go | 14 ++++- api/http/handler/stacks/stack_start.go | 13 ++++- api/http/handler/stacks/stack_stop.go | 3 +- api/http/handler/stacks/stack_update.go | 3 +- .../proxy/factory/docker/access_control.go | 40 ++++++++++---- api/http/proxy/factory/docker/configs.go | 8 +-- api/http/proxy/factory/docker/containers.go | 13 ++--- api/http/proxy/factory/docker/networks.go | 11 ++-- api/http/proxy/factory/docker/secrets.go | 8 +-- api/http/proxy/factory/docker/services.go | 8 +-- api/http/proxy/factory/docker/volumes.go | 10 ++-- api/http/server.go | 1 + api/internal/authorization/access_control.go | 5 +- api/internal/stackutils/stackutils.go | 12 +++++ api/portainer.go | 2 +- app/portainer/views/stacks/edit/stack.html | 3 +- 25 files changed, 245 insertions(+), 99 deletions(-) create mode 100644 api/bolt/migrator/migrate_dbversion26.go create mode 100644 api/internal/stackutils/stackutils.go 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 @@ - + +