mirror of https://github.com/portainer/portainer
feat(stacks): allow to use images from private registries in stacks (#1327)
parent
34d40e4876
commit
b5629c5b1a
|
@ -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 {
|
func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||||
|
@ -29,7 +60,7 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.
|
||||||
return runCommandAndCaptureStdErr(command, args)
|
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 {
|
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
@ -22,6 +23,8 @@ import (
|
||||||
|
|
||||||
// StackHandler represents an HTTP API handler for managing Stack.
|
// StackHandler represents an HTTP API handler for managing Stack.
|
||||||
type StackHandler struct {
|
type StackHandler struct {
|
||||||
|
stackCreationMutex *sync.Mutex
|
||||||
|
stackDeletionMutex *sync.Mutex
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
|
@ -29,17 +32,21 @@ type StackHandler struct {
|
||||||
StackService portainer.StackService
|
StackService portainer.StackService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
|
RegistryService portainer.RegistryService
|
||||||
|
DockerHubService portainer.DockerHubService
|
||||||
StackManager portainer.StackManager
|
StackManager portainer.StackManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStackHandler returns a new instance of StackHandler.
|
// NewStackHandler returns a new instance of StackHandler.
|
||||||
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
|
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
|
||||||
h := &StackHandler{
|
h := &StackHandler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
stackCreationMutex: &sync.Mutex{},
|
||||||
|
stackDeletionMutex: &sync.Mutex{},
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
}
|
}
|
||||||
h.Handle("/{endpointId}/stacks",
|
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",
|
h.Handle("/{endpointId}/stacks",
|
||||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStacks))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStacks))).Methods(http.MethodGet)
|
||||||
h.Handle("/{endpointId}/stacks/{id}",
|
h.Handle("/{endpointId}/stacks/{id}",
|
||||||
|
@ -173,7 +180,31 @@ func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter,
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -275,7 +306,31 @@ func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWri
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -354,7 +409,31 @@ func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -515,7 +594,31 @@ func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
|
@ -589,11 +692,13 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handler.stackDeletionMutex.Lock()
|
||||||
err = handler.StackManager.Remove(stack, endpoint)
|
err = handler.StackManager.Remove(stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
handler.stackDeletionMutex.Unlock()
|
||||||
|
|
||||||
err = handler.StackService.DeleteStack(portainer.StackID(stackID))
|
err = handler.StackService.DeleteStack(portainer.StackID(stackID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -607,3 +712,28 @@ func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Re
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterRegistries filters registries based on user role and team memberships.
|
// 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) {
|
func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) {
|
||||||
|
|
||||||
filteredRegistries := registries
|
filteredRegistries := registries
|
||||||
|
|
|
@ -94,6 +94,8 @@ func (server *Server) Start() error {
|
||||||
stackHandler.ResourceControlService = server.ResourceControlService
|
stackHandler.ResourceControlService = server.ResourceControlService
|
||||||
stackHandler.StackManager = server.StackManager
|
stackHandler.StackManager = server.StackManager
|
||||||
stackHandler.GitService = server.GitService
|
stackHandler.GitService = server.GitService
|
||||||
|
stackHandler.RegistryService = server.RegistryService
|
||||||
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
|
|
||||||
server.Handler = &handler.Handler{
|
server.Handler = &handler.Handler{
|
||||||
AuthHandler: authHandler,
|
AuthHandler: authHandler,
|
||||||
|
|
|
@ -379,6 +379,8 @@ type (
|
||||||
|
|
||||||
// StackManager represents a service to manage stacks.
|
// StackManager represents a service to manage stacks.
|
||||||
StackManager interface {
|
StackManager interface {
|
||||||
|
Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) error
|
||||||
|
Logout(endpoint *Endpoint) error
|
||||||
Deploy(stack *Stack, endpoint *Endpoint) error
|
Deploy(stack *Stack, endpoint *Endpoint) error
|
||||||
Remove(stack *Stack, endpoint *Endpoint) error
|
Remove(stack *Stack, endpoint *Endpoint) error
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue