From b5629c5b1adc13b79fc8441c98481949f6e1d8a5 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 26 Oct 2017 14:22:09 +0200 Subject: [PATCH] feat(stacks): allow to use images from private registries in stacks (#1327) --- api/exec/stack_manager.go | 35 ++++++++- api/http/handler/stack.go | 144 ++++++++++++++++++++++++++++++++++-- api/http/security/filter.go | 2 +- api/http/server.go | 2 + api/portainer.go | 2 + 5 files changed, 175 insertions(+), 10 deletions(-) diff --git a/api/exec/stack_manager.go b/api/exec/stack_manager.go index 3f8b60b79..61051f01c 100644 --- a/api/exec/stack_manager.go +++ b/api/exec/stack_manager.go @@ -21,7 +21,38 @@ func NewStackManager(binaryPath string) *StackManager { } } -// Deploy will execute the Docker stack deploy command +// Login executes the docker login command against a list of registries (including DockerHub). +func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) error { + command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) + for _, registry := range registries { + if registry.Authentication { + registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL) + err := runCommandAndCaptureStdErr(command, registryArgs) + if err != nil { + return err + } + } + } + + if dockerhub.Authentication { + dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password) + err := runCommandAndCaptureStdErr(command, dockerhubArgs) + if err != nil { + return err + } + } + + return nil +} + +// Logout executes the docker logout command. +func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error { + command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) + args = append(args, "logout") + return runCommandAndCaptureStdErr(command, args) +} + +// Deploy executes the docker stack deploy command. func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error { stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint) command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) @@ -29,7 +60,7 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer. return runCommandAndCaptureStdErr(command, args) } -// Remove will execute the Docker stack rm command +// Remove executes the docker stack rm command. func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error { command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint) args = append(args, "stack", "rm", stack.Name) diff --git a/api/http/handler/stack.go b/api/http/handler/stack.go index e3e13930c..f90f68928 100644 --- a/api/http/handler/stack.go +++ b/api/http/handler/stack.go @@ -5,6 +5,7 @@ import ( "path" "strconv" "strings" + "sync" "github.com/asaskevich/govalidator" "github.com/portainer/portainer" @@ -22,6 +23,8 @@ import ( // StackHandler represents an HTTP API handler for managing Stack. type StackHandler struct { + stackCreationMutex *sync.Mutex + stackDeletionMutex *sync.Mutex *mux.Router Logger *log.Logger FileService portainer.FileService @@ -29,17 +32,21 @@ type StackHandler struct { StackService portainer.StackService EndpointService portainer.EndpointService ResourceControlService portainer.ResourceControlService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService StackManager portainer.StackManager } // NewStackHandler returns a new instance of StackHandler. func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler { h := &StackHandler{ - Router: mux.NewRouter(), - Logger: log.New(os.Stderr, "", log.LstdFlags), + Router: mux.NewRouter(), + stackCreationMutex: &sync.Mutex{}, + stackDeletionMutex: &sync.Mutex{}, + Logger: log.New(os.Stderr, "", log.LstdFlags), } h.Handle("/{endpointId}/stacks", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostStacks))).Methods(http.MethodPost) + bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostStacks))).Methods(http.MethodPost) h.Handle("/{endpointId}/stacks", bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStacks))).Methods(http.MethodGet) h.Handle("/{endpointId}/stacks/{id}", @@ -173,7 +180,31 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter, return } - err = handler.StackManager.Deploy(stack, endpoint) + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + dockerhub, err := handler.DockerHubService.DockerHub() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + filteredRegistries, err := security.FilterRegistries(registries, securityContext) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -275,7 +306,31 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri return } - err = handler.StackManager.Deploy(stack, endpoint) + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + dockerhub, err := handler.DockerHubService.DockerHub() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + filteredRegistries, err := security.FilterRegistries(registries, securityContext) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -354,7 +409,31 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r return } - err = handler.StackManager.Deploy(stack, endpoint) + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + dockerhub, err := handler.DockerHubService.DockerHub() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + filteredRegistries, err := security.FilterRegistries(registries, securityContext) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -515,7 +594,31 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque return } - err = handler.StackManager.Deploy(stack, endpoint) + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + dockerhub, err := handler.DockerHubService.DockerHub() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + filteredRegistries, err := security.FilterRegistries(registries, securityContext) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.deployStack(endpoint, stack, dockerhub, filteredRegistries) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return @@ -589,11 +692,13 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re return } + handler.stackDeletionMutex.Lock() err = handler.StackManager.Remove(stack, endpoint) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } + handler.stackDeletionMutex.Unlock() err = handler.StackService.DeleteStack(portainer.StackID(stackID)) if err != nil { @@ -607,3 +712,28 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re return } } + +func (handler *StackHandler) deployStack(endpoint *portainer.Endpoint, stack *portainer.Stack, dockerhub *portainer.DockerHub, registries []portainer.Registry) error { + handler.stackCreationMutex.Lock() + + err := handler.StackManager.Login(dockerhub, registries, endpoint) + if err != nil { + handler.stackCreationMutex.Unlock() + return err + } + + err = handler.StackManager.Deploy(stack, endpoint) + if err != nil { + handler.stackCreationMutex.Unlock() + return err + } + + err = handler.StackManager.Logout(endpoint) + if err != nil { + handler.stackCreationMutex.Unlock() + return err + } + + handler.stackCreationMutex.Unlock() + return nil +} diff --git a/api/http/security/filter.go b/api/http/security/filter.go index 7e7f56c7c..9f28f19c0 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -61,7 +61,7 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po } // FilterRegistries filters registries based on user role and team memberships. -// Non administrator users only have access to authorized endpoints. +// Non administrator users only have access to authorized registries. func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) { filteredRegistries := registries diff --git a/api/http/server.go b/api/http/server.go index 93d874cb3..d0402bed4 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -94,6 +94,8 @@ func (server *Server) Start() error { stackHandler.ResourceControlService = server.ResourceControlService stackHandler.StackManager = server.StackManager stackHandler.GitService = server.GitService + stackHandler.RegistryService = server.RegistryService + stackHandler.DockerHubService = server.DockerHubService server.Handler = &handler.Handler{ AuthHandler: authHandler, diff --git a/api/portainer.go b/api/portainer.go index e1bdf9455..1e9b2f3a2 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -379,6 +379,8 @@ type ( // StackManager represents a service to manage stacks. StackManager interface { + Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) error + Logout(endpoint *Endpoint) error Deploy(stack *Stack, endpoint *Endpoint) error Remove(stack *Stack, endpoint *Endpoint) error }