2021-08-17 01:12:07 +00:00
package stacks
import (
"fmt"
"log"
"net/http"
"time"
"github.com/asaskevich/govalidator"
2021-09-07 00:37:26 +00:00
"github.com/pkg/errors"
2021-08-17 01:12:07 +00:00
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"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"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
2021-09-07 00:37:26 +00:00
k "github.com/portainer/portainer/api/kubernetes"
2021-08-17 01:12:07 +00:00
)
type stackGitRedployPayload struct {
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
Env [ ] portainer . Pair
}
func ( payload * stackGitRedployPayload ) Validate ( r * http . Request ) error {
if govalidator . IsNull ( payload . RepositoryReferenceName ) {
payload . RepositoryReferenceName = defaultGitReferenceName
}
return nil
}
2021-09-07 00:37:26 +00:00
// @id StackGitRedeploy
// @summary Redeploy a stack
// @description Pull and redeploy a stack via Git
2021-10-11 23:12:08 +00:00
// @description **Access policy**: authenticated
2021-09-07 00:37:26 +00:00
// @tags stacks
2021-11-30 02:31:16 +00:00
// @security ApiKeyAuth
2021-09-07 00:37:26 +00:00
// @security jwt
// @accept json
// @produce json
// @param id path int true "Stack identifier"
2021-09-20 00:14:22 +00:00
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated environment(endpoint) identifier. Use this optional parameter to set the environment(endpoint) identifier used by the stack."
2021-09-07 00:37:26 +00:00
// @param body body stackGitRedployPayload true "Git configs for pull and redeploy a stack"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Not found"
// @failure 500 "Server error"
2021-09-13 03:42:53 +00:00
// @router /stacks/{id}/git/redeploy [put]
2021-08-17 01:12:07 +00:00
func ( handler * Handler ) stackGitRedeploy ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
stackID , err := request . RetrieveNumericRouteVariableValue ( r , "id" )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid stack identifier route variable" , Err : err }
}
stack , err := handler . DataStore . Stack ( ) . Stack ( portainer . StackID ( stackID ) )
if err == bolterrors . ErrObjectNotFound {
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 { StatusCode : http . StatusInternalServerError , Message : "Unable to find a stack with the specified identifier inside the database" , Err : err }
}
if stack . GitConfig == nil {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Stack is not created from git" , Err : err }
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
2021-09-20 00:14:22 +00:00
// The EndpointID property is not available for these stacks, this API environment(endpoint)
// can use the optional EndpointID query parameter to associate a valid environment(endpoint) identifier to the stack.
2021-08-17 01:12:07 +00:00
endpointID , err := request . RetrieveNumericQueryParameter ( r , "endpointId" , true )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid query parameter: endpointId" , Err : err }
}
if endpointID != int ( stack . EndpointID ) {
stack . EndpointID = portainer . EndpointID ( endpointID )
}
endpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( stack . EndpointID )
if err == bolterrors . ErrObjectNotFound {
2021-09-08 08:42:17 +00:00
return & httperror . HandlerError { StatusCode : http . StatusNotFound , Message : "Unable to find the environment associated to the stack inside the database" , Err : err }
2021-08-17 01:12:07 +00:00
} else if err != nil {
2021-09-08 08:42:17 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find the environment associated to the stack inside the database" , Err : err }
2021-08-17 01:12:07 +00:00
}
err = handler . requestBouncer . AuthorizedEndpointOperation ( r , endpoint )
if err != nil {
2021-09-08 08:42:17 +00:00
return & httperror . HandlerError { StatusCode : http . StatusForbidden , Message : "Permission denied to access environment" , Err : err }
2021-08-17 01:12:07 +00:00
}
securityContext , err := security . RetrieveRestrictedRequestContext ( r )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to retrieve info from request context" , Err : err }
}
2021-09-07 00:37:26 +00:00
//only check resource control when it is a DockerSwarmStack or a DockerComposeStack
if stack . Type == portainer . DockerSwarmStack || stack . Type == portainer . DockerComposeStack {
resourceControl , err := handler . DataStore . ResourceControl ( ) . ResourceControlByResourceIDAndType ( stackutils . ResourceControlID ( stack . EndpointID , stack . Name ) , portainer . StackResourceControl )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to retrieve a resource control associated to the stack" , Err : err }
}
access , err := handler . userCanAccessStack ( securityContext , endpoint . ID , resourceControl )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to verify user authorizations to validate stack access" , Err : err }
}
if ! access {
return & httperror . HandlerError { StatusCode : http . StatusForbidden , Message : "Access denied to resource" , Err : httperrors . ErrResourceAccessDenied }
}
2021-08-17 01:12:07 +00:00
}
var payload stackGitRedployPayload
err = request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid request payload" , Err : err }
}
stack . GitConfig . ReferenceName = payload . RepositoryReferenceName
stack . Env = payload . Env
backupProjectPath := fmt . Sprintf ( "%s-old" , stack . ProjectPath )
err = filesystem . MoveDirectory ( stack . ProjectPath , backupProjectPath )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to move git repository directory" , Err : err }
}
repositoryUsername := ""
repositoryPassword := ""
if payload . RepositoryAuthentication {
repositoryPassword = payload . RepositoryPassword
if repositoryPassword == "" && stack . GitConfig != nil && stack . GitConfig . Authentication != nil {
repositoryPassword = stack . GitConfig . Authentication . Password
}
repositoryUsername = payload . RepositoryUsername
}
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 )
}
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to clone git repository" , Err : 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
}
2021-09-07 00:37:26 +00:00
newHash , err := handler . GitService . LatestCommitID ( stack . GitConfig . URL , stack . GitConfig . ReferenceName , repositoryUsername , repositoryPassword )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable get latest commit id" , Err : errors . WithMessagef ( err , "failed to fetch latest commit id of the stack %v" , stack . ID ) }
}
stack . GitConfig . ConfigHash = newHash
user , err := handler . DataStore . User ( ) . User ( securityContext . UserID )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Cannot find context user" , Err : errors . Wrap ( err , "failed to fetch the user" ) }
}
stack . UpdatedBy = user . Username
stack . UpdateDate = time . Now ( ) . Unix ( )
stack . Status = portainer . StackStatusActive
2021-08-17 01:12:07 +00:00
err = handler . DataStore . Stack ( ) . UpdateStack ( stack . ID , stack )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist the stack changes inside the database" , Err : errors . Wrap ( err , "failed to update the stack" ) }
2021-08-17 01:12:07 +00:00
}
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 {
2021-09-07 00:37:26 +00:00
switch stack . Type {
case portainer . DockerSwarmStack :
2021-08-17 01:12:07 +00:00
config , httpErr := handler . createSwarmDeployConfig ( r , stack , endpoint , false )
if httpErr != nil {
return httpErr
}
2021-09-07 00:37:26 +00:00
if err := handler . deploySwarmStack ( config ) ; err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
}
2021-09-07 00:37:26 +00:00
case portainer . DockerComposeStack :
config , httpErr := handler . createComposeDeployConfig ( r , stack , endpoint )
if httpErr != nil {
return httpErr
}
2021-08-17 01:12:07 +00:00
2021-09-07 00:37:26 +00:00
if err := handler . deployComposeStack ( config ) ; err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
}
2021-08-17 01:12:07 +00:00
2021-09-07 00:37:26 +00:00
case portainer . KubernetesStack :
if stack . Namespace == "" {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Invalid namespace" , Err : errors . New ( "Namespace must not be empty when redeploying kubernetes stacks" ) }
}
2021-09-29 23:58:10 +00:00
tokenData , err := security . RetrieveTokenData ( r )
2021-09-07 00:37:26 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Failed to retrieve user token data" , Err : err }
2021-09-07 00:37:26 +00:00
}
2021-09-29 23:58:10 +00:00
_ , err = handler . deployKubernetesStack ( tokenData . ID , endpoint , stack , k . KubeAppLabels {
StackID : int ( stack . ID ) ,
StackName : stack . Name ,
Owner : tokenData . Username ,
Kind : "git" ,
2021-09-07 00:37:26 +00:00
} )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to redeploy Kubernetes stack" , Err : errors . WithMessage ( err , "failed to deploy kube application" ) }
}
2021-08-17 01:12:07 +00:00
2021-09-07 00:37:26 +00:00
default :
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unsupported stack" , Err : errors . Errorf ( "unsupported stack type: %v" , stack . Type ) }
2021-08-17 01:12:07 +00:00
}
return nil
}