2018-06-11 13:13:19 +00:00
package stacks
import (
2021-02-23 20:18:05 +00:00
"fmt"
2018-06-11 13:13:19 +00:00
"net/http"
2019-10-07 03:12:21 +00:00
"path"
2018-06-20 17:55:00 +00:00
"strconv"
2021-01-11 23:38:49 +00:00
"time"
2018-06-11 13:13:19 +00:00
2021-09-07 00:37:26 +00:00
"github.com/pkg/errors"
2018-06-11 13:13:19 +00:00
"github.com/asaskevich/govalidator"
2018-09-10 10:01:38 +00:00
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
2021-01-11 23:38:49 +00:00
portainer "github.com/portainer/portainer/api"
2019-03-21 01:20:14 +00:00
"github.com/portainer/portainer/api/filesystem"
2021-08-17 01:12:07 +00:00
gittypes "github.com/portainer/portainer/api/git/types"
2019-03-21 01:20:14 +00:00
"github.com/portainer/portainer/api/http/security"
2018-06-11 13:13:19 +00:00
)
type swarmStackFromFileContentPayload struct {
2021-02-23 03:21:39 +00:00
// Name of the stack
Name string ` example:"myStack" validate:"required" `
// Swarm cluster identifier
SwarmID string ` example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required" `
// Content of the Stack file
StackFileContent string ` example:"version: 3\n services:\n web:\n image:nginx" validate:"required" `
// A list of environment variables used during stack deployment
Env [ ] portainer . Pair
2018-06-11 13:13:19 +00:00
}
func ( payload * swarmStackFromFileContentPayload ) Validate ( r * http . Request ) error {
if govalidator . IsNull ( payload . Name ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid stack name" )
2018-06-11 13:13:19 +00:00
}
if govalidator . IsNull ( payload . SwarmID ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid Swarm ID" )
2018-06-11 13:13:19 +00:00
}
if govalidator . IsNull ( payload . StackFileContent ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid stack file content" )
2018-06-11 13:13:19 +00:00
}
return nil
}
2019-11-12 23:41:42 +00:00
func ( handler * Handler ) createSwarmStackFromFileContent ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2018-06-11 13:13:19 +00:00
var payload swarmStackFromFileContentPayload
err := request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid request payload" , err }
}
2021-07-22 21:53:42 +00:00
payload . Name = handler . SwarmStackManager . NormalizeStackName ( payload . Name )
2021-02-23 20:18:05 +00:00
isUnique , err := handler . checkUniqueName ( endpoint , payload . Name , 0 , true )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-02-23 20:18:05 +00:00
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to check for name collision" , err }
2018-06-11 13:13:19 +00:00
}
2021-02-23 20:18:05 +00:00
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 ) }
2018-06-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
stackID := handler . DataStore . Stack ( ) . GetNextIdentifier ( )
2018-06-11 13:13:19 +00:00
stack := & portainer . Stack {
2021-01-11 23:38:49 +00:00
ID : portainer . StackID ( stackID ) ,
Name : payload . Name ,
Type : portainer . DockerSwarmStack ,
SwarmID : payload . SwarmID ,
EndpointID : endpoint . ID ,
EntryPoint : filesystem . ComposeFileDefaultName ,
Env : payload . Env ,
Status : portainer . StackStatusActive ,
CreationDate : time . Now ( ) . Unix ( ) ,
2018-06-11 13:13:19 +00:00
}
2018-06-20 17:55:00 +00:00
stackFolder := strconv . Itoa ( int ( stack . ID ) )
projectPath , err := handler . FileService . StoreStackFileFromBytes ( stackFolder , stack . EntryPoint , [ ] byte ( payload . StackFileContent ) )
2018-06-11 13:13:19 +00:00
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to persist Compose file on disk" , err }
}
stack . ProjectPath = projectPath
doCleanUp := true
defer handler . cleanUp ( stack , & doCleanUp )
config , configErr := handler . createSwarmDeployConfig ( r , stack , endpoint , false )
if configErr != nil {
return configErr
}
err = handler . deploySwarmStack ( config )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , err . Error ( ) , err }
}
2021-01-11 23:38:49 +00:00
stack . CreatedBy = config . user . Username
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . CreateStack ( stack )
2018-06-11 13:13:19 +00:00
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to persist the stack inside the database" , err }
}
doCleanUp = false
2019-11-12 23:41:42 +00:00
return handler . decorateStackResponse ( w , stack , userID )
2018-06-11 13:13:19 +00:00
}
type swarmStackFromGitRepositoryPayload struct {
2021-02-23 03:21:39 +00:00
// Name of the stack
Name string ` example:"myStack" validate:"required" `
// Swarm cluster identifier
SwarmID string ` example:"jpofkc0i9uo9wtx1zesuk649w" validate:"required" `
// A list of environment variables used during stack deployment
Env [ ] portainer . Pair
// URL of a Git repository hosting the Stack file
RepositoryURL string ` example:"https://github.com/openfaas/faas" validate:"required" `
// Reference name of a Git repository hosting the Stack file
RepositoryReferenceName string ` example:"refs/heads/master" `
// Use basic authentication to clone the Git repository
RepositoryAuthentication bool ` example:"true" `
// Username used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryUsername string ` example:"myGitUsername" `
// Password used in basic authentication. Required when RepositoryAuthentication is true.
RepositoryPassword string ` example:"myGitPassword" `
// Path to the Stack file inside the Git repository
2021-08-17 01:12:07 +00:00
ComposeFile string ` example:"docker-compose.yml" default:"docker-compose.yml" `
// Applicable when deploying with multiple stack files
AdditionalFiles [ ] string ` example:"[nz.compose.yml, uat.compose.yml]" `
// Optional auto update configuration
AutoUpdate * portainer . StackAutoUpdate
2018-06-11 13:13:19 +00:00
}
func ( payload * swarmStackFromGitRepositoryPayload ) Validate ( r * http . Request ) error {
if govalidator . IsNull ( payload . Name ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid stack name" )
2018-06-11 13:13:19 +00:00
}
if govalidator . IsNull ( payload . SwarmID ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid Swarm ID" )
2018-06-11 13:13:19 +00:00
}
if govalidator . IsNull ( payload . RepositoryURL ) || ! govalidator . IsURL ( payload . RepositoryURL ) {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid repository URL. Must correspond to a valid URL format" )
2018-06-11 13:13:19 +00:00
}
2021-08-17 01:12:07 +00:00
if govalidator . IsNull ( payload . RepositoryReferenceName ) {
payload . RepositoryReferenceName = defaultGitReferenceName
2018-06-11 13:13:19 +00:00
}
2021-08-17 01:12:07 +00:00
if payload . RepositoryAuthentication && govalidator . IsNull ( payload . RepositoryPassword ) {
return errors . New ( "Invalid repository credentials. Password must be specified when authentication is enabled" )
}
if err := validateStackAutoUpdate ( payload . AutoUpdate ) ; err != nil {
return err
2018-06-11 13:13:19 +00:00
}
return nil
}
2019-11-12 23:41:42 +00:00
func ( handler * Handler ) createSwarmStackFromGitRepository ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2018-06-11 13:13:19 +00:00
var payload swarmStackFromGitRepositoryPayload
err := request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid request payload" , Err : err }
2018-06-11 13:13:19 +00:00
}
2021-07-22 21:53:42 +00:00
payload . Name = handler . SwarmStackManager . NormalizeStackName ( payload . Name )
2021-02-23 20:18:05 +00:00
isUnique , err := handler . checkUniqueName ( endpoint , payload . Name , 0 , true )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to check for name collision" , Err : err }
2018-06-11 13:13:19 +00:00
}
2021-02-23 20:18:05 +00:00
if ! isUnique {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "A stack with the name '%s' already exists" , payload . Name ) , Err : errStackAlreadyExists }
}
//make sure the webhook ID is unique
if payload . AutoUpdate != nil && payload . AutoUpdate . Webhook != "" {
isUnique , err := handler . checkUniqueWebhookID ( payload . AutoUpdate . Webhook )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to check for webhook ID collision" , Err : err }
}
if ! isUnique {
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : fmt . Sprintf ( "Webhook ID: %s already exists" , payload . AutoUpdate . Webhook ) , Err : errWebhookIDAlreadyExists }
}
2018-06-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
stackID := handler . DataStore . Stack ( ) . GetNextIdentifier ( )
2018-06-11 13:13:19 +00:00
stack := & portainer . Stack {
2021-08-17 01:12:07 +00:00
ID : portainer . StackID ( stackID ) ,
Name : payload . Name ,
Type : portainer . DockerSwarmStack ,
SwarmID : payload . SwarmID ,
EndpointID : endpoint . ID ,
EntryPoint : payload . ComposeFile ,
AdditionalFiles : payload . AdditionalFiles ,
AutoUpdate : payload . AutoUpdate ,
GitConfig : & gittypes . RepoConfig {
URL : payload . RepositoryURL ,
ReferenceName : payload . RepositoryReferenceName ,
ConfigFilePath : payload . ComposeFile ,
} ,
2021-01-11 23:38:49 +00:00
Env : payload . Env ,
Status : portainer . StackStatusActive ,
CreationDate : time . Now ( ) . Unix ( ) ,
2018-06-11 13:13:19 +00:00
}
2021-08-17 01:12:07 +00:00
if payload . RepositoryAuthentication {
stack . GitConfig . Authentication = & gittypes . GitAuthentication {
Username : payload . RepositoryUsername ,
Password : payload . RepositoryPassword ,
}
}
2018-06-25 11:48:28 +00:00
projectPath := handler . FileService . GetStackProjectPath ( strconv . Itoa ( int ( stack . ID ) ) )
2018-06-11 13:13:19 +00:00
stack . ProjectPath = projectPath
2018-07-24 14:11:35 +00:00
doCleanUp := true
defer handler . cleanUp ( stack , & doCleanUp )
2021-08-17 01:12:07 +00:00
err = handler . clone ( projectPath , payload . RepositoryURL , payload . RepositoryReferenceName , payload . RepositoryAuthentication , payload . RepositoryUsername , payload . RepositoryPassword )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-06-15 21:11:35 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to clone git repository" , Err : err }
2018-06-11 13:13:19 +00:00
}
2021-08-17 01:12:07 +00:00
commitId , err := handler . latestCommitID ( payload . RepositoryURL , payload . RepositoryReferenceName , payload . RepositoryAuthentication , payload . RepositoryUsername , payload . RepositoryPassword )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to fetch git repository id" , Err : err }
}
stack . GitConfig . ConfigHash = commitId
2018-06-11 13:13:19 +00:00
config , configErr := handler . createSwarmDeployConfig ( r , stack , endpoint , false )
if configErr != nil {
return configErr
}
err = handler . deploySwarmStack ( config )
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
}
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
}
stack . AutoUpdate . JobID = jobID
2018-06-11 13:13:19 +00:00
}
2021-01-11 23:38:49 +00:00
stack . CreatedBy = config . user . Username
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . CreateStack ( stack )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist the stack inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
}
doCleanUp = false
2019-11-12 23:41:42 +00:00
return handler . decorateStackResponse ( w , stack , userID )
2018-06-11 13:13:19 +00:00
}
type swarmStackFromFileUploadPayload struct {
Name string
SwarmID string
StackFileContent [ ] byte
Env [ ] portainer . Pair
}
func ( payload * swarmStackFromFileUploadPayload ) Validate ( r * http . Request ) error {
name , err := request . RetrieveMultiPartFormValue ( r , "Name" , false )
if err != nil {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid stack name" )
2018-06-11 13:13:19 +00:00
}
payload . Name = name
swarmID , err := request . RetrieveMultiPartFormValue ( r , "SwarmID" , false )
if err != nil {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid Swarm ID" )
2018-06-11 13:13:19 +00:00
}
payload . SwarmID = swarmID
2018-09-10 10:01:38 +00:00
composeFileContent , _ , err := request . RetrieveMultiPartFormFile ( r , "file" )
2018-06-11 13:13:19 +00:00
if err != nil {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid Compose file. Ensure that the Compose file is uploaded correctly" )
2018-06-11 13:13:19 +00:00
}
payload . StackFileContent = composeFileContent
var env [ ] portainer . Pair
err = request . RetrieveMultiPartFormJSONValue ( r , "Env" , & env , true )
if err != nil {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid Env parameter" )
2018-06-11 13:13:19 +00:00
}
payload . Env = env
return nil
}
2019-11-12 23:41:42 +00:00
func ( handler * Handler ) createSwarmStackFromFileUpload ( w http . ResponseWriter , r * http . Request , endpoint * portainer . Endpoint , userID portainer . UserID ) * httperror . HandlerError {
2018-06-11 13:13:19 +00:00
payload := & swarmStackFromFileUploadPayload { }
err := payload . Validate ( r )
if err != nil {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid request payload" , err }
}
2021-07-22 21:53:42 +00:00
payload . Name = handler . SwarmStackManager . NormalizeStackName ( payload . Name )
2021-02-23 20:18:05 +00:00
isUnique , err := handler . checkUniqueName ( endpoint , payload . Name , 0 , true )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-02-23 20:18:05 +00:00
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to check for name collision" , err }
2018-06-11 13:13:19 +00:00
}
2021-02-23 20:18:05 +00:00
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 ) }
2018-06-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
stackID := handler . DataStore . Stack ( ) . GetNextIdentifier ( )
2018-06-11 13:13:19 +00:00
stack := & portainer . Stack {
2021-01-11 23:38:49 +00:00
ID : portainer . StackID ( stackID ) ,
Name : payload . Name ,
Type : portainer . DockerSwarmStack ,
SwarmID : payload . SwarmID ,
EndpointID : endpoint . ID ,
EntryPoint : filesystem . ComposeFileDefaultName ,
Env : payload . Env ,
Status : portainer . StackStatusActive ,
CreationDate : time . Now ( ) . Unix ( ) ,
2018-06-11 13:13:19 +00:00
}
2018-06-20 17:55:00 +00:00
stackFolder := strconv . Itoa ( int ( stack . ID ) )
projectPath , err := handler . FileService . StoreStackFileFromBytes ( stackFolder , stack . EntryPoint , [ ] byte ( payload . StackFileContent ) )
2018-06-11 13:13:19 +00:00
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to persist Compose file on disk" , err }
}
stack . ProjectPath = projectPath
doCleanUp := true
defer handler . cleanUp ( stack , & doCleanUp )
config , configErr := handler . createSwarmDeployConfig ( r , stack , endpoint , false )
if configErr != nil {
return configErr
}
err = handler . deploySwarmStack ( config )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , err . Error ( ) , err }
}
2021-01-11 23:38:49 +00:00
stack . CreatedBy = config . user . Username
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . CreateStack ( stack )
2018-06-11 13:13:19 +00:00
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to persist the stack inside the database" , err }
}
doCleanUp = false
2019-11-12 23:41:42 +00:00
return handler . decorateStackResponse ( w , stack , userID )
2018-06-11 13:13:19 +00:00
}
type swarmStackDeploymentConfig struct {
stack * portainer . Stack
endpoint * portainer . Endpoint
registries [ ] portainer . Registry
prune bool
2019-10-07 03:12:21 +00:00
isAdmin bool
2020-07-22 18:38:45 +00:00
user * portainer . User
2018-06-11 13:13:19 +00:00
}
func ( handler * Handler ) createSwarmDeployConfig ( r * http . Request , stack * portainer . Stack , endpoint * portainer . Endpoint , prune bool ) ( * swarmStackDeploymentConfig , * httperror . HandlerError ) {
securityContext , err := security . RetrieveRestrictedRequestContext ( r )
if err != nil {
return nil , & httperror . HandlerError { http . StatusInternalServerError , "Unable to retrieve info from request context" , err }
}
2021-07-14 09:15:21 +00:00
user , err := handler . DataStore . User ( ) . User ( securityContext . UserID )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-07-14 09:15:21 +00:00
return nil , & httperror . HandlerError { http . StatusInternalServerError , "Unable to load user information from the database" , err }
2018-06-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
registries , err := handler . DataStore . Registry ( ) . Registries ( )
2018-06-11 13:13:19 +00:00
if err != nil {
return nil , & httperror . HandlerError { http . StatusInternalServerError , "Unable to retrieve registries from the database" , err }
}
2021-07-14 09:15:21 +00:00
filteredRegistries := security . FilterRegistries ( registries , user , securityContext . UserMemberships , endpoint . ID )
2020-07-22 18:38:45 +00:00
2018-06-11 13:13:19 +00:00
config := & swarmStackDeploymentConfig {
stack : stack ,
endpoint : endpoint ,
registries : filteredRegistries ,
prune : prune ,
2019-10-07 03:12:21 +00:00
isAdmin : securityContext . IsAdmin ,
2020-07-22 18:38:45 +00:00
user : user ,
2018-06-11 13:13:19 +00:00
}
return config , nil
}
func ( handler * Handler ) deploySwarmStack ( config * swarmStackDeploymentConfig ) error {
2020-07-22 18:38:45 +00:00
isAdminOrEndpointAdmin , err := handler . userIsAdminOrEndpointAdmin ( config . user , config . endpoint . ID )
if err != nil {
2021-09-07 00:37:26 +00:00
return errors . Wrap ( err , "failed to validate user admin privileges" )
2020-07-22 18:38:45 +00:00
}
2021-02-09 08:09:06 +00:00
settings := & config . endpoint . SecuritySettings
2020-07-22 18:38:45 +00:00
if ! settings . AllowBindMountsForRegularUsers && ! isAdminOrEndpointAdmin {
2021-08-17 01:12:07 +00:00
for _ , file := range append ( [ ] string { config . stack . EntryPoint } , config . stack . AdditionalFiles ... ) {
path := path . Join ( config . stack . ProjectPath , file )
stackContent , err := handler . FileService . GetFileContent ( path )
if err != nil {
2021-09-07 00:37:26 +00:00
return errors . WithMessage ( err , "failed to get stack file content" )
2021-08-17 01:12:07 +00:00
}
err = handler . isValidStackFile ( stackContent , settings )
if err != nil {
2021-09-07 00:37:26 +00:00
return errors . WithMessage ( err , "swarm stack file content validation failed" )
2021-08-17 01:12:07 +00:00
}
2019-10-07 03:12:21 +00:00
}
}
2021-09-07 00:37:26 +00:00
return handler . StackDeployer . DeploySwarmStack ( config . stack , config . endpoint , config . registries , config . prune )
2018-06-11 13:13:19 +00:00
}