1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-20 13:59:40 +02:00

feat(stacks): support automated sync for stacks [EE-248] (#5340)

This commit is contained in:
Dmitry Salakhov 2021-08-17 13:12:07 +12:00 committed by GitHub
parent 5fe90db36a
commit bcccdfb669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2680 additions and 469 deletions

View file

@ -2,10 +2,7 @@ package stacks
import (
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
@ -13,22 +10,28 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type updateStackGitPayload struct {
type stackGitUpdatePayload struct {
AutoUpdate *portainer.StackAutoUpdate
Env []portainer.Pair
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
}
func (payload *updateStackGitPayload) Validate(r *http.Request) error {
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled")
func (payload *stackGitUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
return nil
}
@ -53,18 +56,23 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error {
func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
var payload stackGitUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.GitConfig == nil {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is not created from git", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if stack.GitConfig == nil {
msg := "No Git config in the found stack"
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: msg, Err: errors.New(msg)}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@ -72,7 +80,7 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@ -80,117 +88,75 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
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}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
var payload updateStackGitPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
//update retrieved stack data based on the payload
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.AutoUpdate = payload.AutoUpdate
stack.Env = payload.Env
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to move git repository directory", err}
stack.GitConfig.Authentication = nil
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
password = stack.GitConfig.Authentication.Password
}
stack.GitConfig.Authentication = &gittypes.GitAuthentication{
Username: payload.RepositoryUsername,
Password: password,
}
}
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err = handler.GitService.CloneRepository(stack.ProjectPath, stack.GitConfig.URL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
restoreError := filesystem.MoveDirectory(backupProjectPath, stack.ProjectPath)
if restoreError != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: failed restoring backup folder]", restoreError)
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err}
}
defer func() {
err = handler.FileService.RemoveDirectory(backupProjectPath)
if err != nil {
log.Printf("[WARN] [http,stacks,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}()
httpErr := handler.deployStack(r, stack, endpoint)
if httpErr != nil {
return httpErr
stack.AutoUpdate.JobID = jobID
}
//save the updated stack to DB
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
// sanitize password in the http response to minimise possible security leaks
stack.GitConfig.Authentication.Password = ""
}
return response.JSON(w, stack)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.Type == portainer.DockerSwarmStack {
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if httpErr != nil {
return httpErr
}
err := handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
if httpErr != nil {
return httpErr
}
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
}
stack.UpdateDate = time.Now().Unix()
stack.UpdatedBy = config.user.Username
stack.Status = portainer.StackStatusActive
return nil
}