2021-06-15 21:11:35 +00:00
package stacks
import (
"net/http"
2021-09-29 23:58:10 +00:00
"time"
2021-06-15 21:11:35 +00:00
"github.com/asaskevich/govalidator"
2021-09-29 23:58:10 +00:00
"github.com/pkg/errors"
2021-06-15 21:11:35 +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"
2021-08-17 01:12:07 +00:00
gittypes "github.com/portainer/portainer/api/git/types"
2021-06-15 21:11:35 +00:00
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
2021-08-17 01:12:07 +00:00
type stackGitUpdatePayload struct {
AutoUpdate * portainer . StackAutoUpdate
Env [ ] portainer . Pair
2021-06-15 21:11:35 +00:00
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
}
2021-08-17 01:12:07 +00:00
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
2021-06-15 21:11:35 +00:00
}
return nil
}
2021-07-21 23:40:53 +00:00
// @id StackUpdateGit
2021-09-07 00:37:26 +00:00
// @summary Update a stack's Git configs
// @description Update the Git settings in a stack, e.g., RepositoryReferenceName and AutoUpdate
2021-10-11 23:12:08 +00:00
// @description **Access policy**: authenticated
2021-07-21 23:40:53 +00:00
// @tags stacks
2021-11-30 02:31:16 +00:00
// @security ApiKeyAuth
2021-07-21 23:40:53 +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 stackGitUpdatePayload true "Git configs for pull and redeploy a stack"
2021-07-21 23:40:53 +00:00
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Not found"
// @failure 500 "Server error"
2021-12-02 01:54:38 +00:00
// @router /stacks/{id}/git [post]
2021-06-15 21:11:35 +00:00
func ( handler * Handler ) stackUpdateGit ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
stackID , err := request . RetrieveNumericRouteVariableValue ( r , "id" )
if err != nil {
2021-08-17 01:12:07 +00:00
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 }
2021-06-15 21:11:35 +00:00
}
stack , err := handler . DataStore . Stack ( ) . Stack ( portainer . StackID ( stackID ) )
if err == bolterrors . ErrObjectNotFound {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusNotFound , Message : "Unable to find a stack with the specified identifier inside the database" , Err : err }
2021-06-15 21:11:35 +00:00
} else if err != nil {
2021-08-17 01:12:07 +00:00
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 ) }
2021-06-15 21:11:35 +00:00
}
// 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-06-15 21:11:35 +00:00
endpointID , err := request . RetrieveNumericQueryParameter ( r , "endpointId" , true )
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid query parameter: endpointId" , Err : err }
2021-06-15 21:11:35 +00:00
}
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-06-15 21:11:35 +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-06-15 21:11:35 +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-06-15 21:11:35 +00:00
}
2021-09-29 23:58:10 +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 }
}
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" ) }
}
2021-09-07 00:37:26 +00:00
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-06-15 21:11:35 +00:00
}
2021-08-17 01:12:07 +00:00
//stop the autoupdate job if there is any
if stack . AutoUpdate != nil {
stopAutoupdate ( stack . ID , stack . AutoUpdate . JobID , * handler . Scheduler )
2021-06-15 21:11:35 +00:00
}
2021-08-17 01:12:07 +00:00
//update retrieved stack data based on the payload
2021-06-15 21:11:35 +00:00
stack . GitConfig . ReferenceName = payload . RepositoryReferenceName
2021-08-17 01:12:07 +00:00
stack . AutoUpdate = payload . AutoUpdate
stack . Env = payload . Env
2021-09-29 23:58:10 +00:00
stack . UpdatedBy = user . Username
stack . UpdateDate = time . Now ( ) . Unix ( )
2021-08-17 01:12:07 +00:00
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 ,
2021-06-15 21:11:35 +00:00
}
2021-10-25 21:19:05 +00:00
_ , err = handler . GitService . LatestCommitID ( stack . GitConfig . URL , stack . GitConfig . ReferenceName , stack . GitConfig . Authentication . Username , stack . GitConfig . Authentication . Password )
if err != nil {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to fetch git repository" , Err : err }
}
2021-11-11 22:52:09 +00:00
} else {
stack . GitConfig . Authentication = nil
2021-06-15 21:11:35 +00:00
}
2021-08-17 01:12:07 +00:00
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
2021-06-22 00:58:54 +00:00
}
2021-08-17 01:12:07 +00:00
stack . AutoUpdate . JobID = jobID
2021-06-15 21:11:35 +00:00
}
2021-08-17 01:12:07 +00:00
//save the updated stack to DB
2021-06-15 21:11:35 +00:00
err = handler . DataStore . Stack ( ) . UpdateStack ( stack . ID , stack )
if err != nil {
2021-08-17 01:12:07 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist the stack changes inside the database" , Err : err }
2021-06-15 21:11:35 +00:00
}
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 = ""
2021-06-15 21:11:35 +00:00
}
2021-08-17 01:12:07 +00:00
return response . JSON ( w , stack )
2021-06-15 21:11:35 +00:00
}