2018-06-11 13:13:19 +00:00
package stacks
import (
"net/http"
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"
"github.com/portainer/libhttp/response"
2021-01-11 23:38:49 +00:00
portainer "github.com/portainer/portainer/api"
2020-07-07 21:57:52 +00:00
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
2021-02-23 20:18:05 +00:00
"github.com/portainer/portainer/api/internal/stackutils"
2018-06-11 13:13:19 +00:00
)
type updateComposeStackPayload struct {
2021-02-23 03:21:39 +00:00
// New content of the Stack file
StackFileContent string ` example:"version: 3\n services:\n web:\n image:nginx" `
2021-09-20 00:14:22 +00:00
// A list of environment(endpoint) variables used during stack deployment
2021-02-23 03:21:39 +00:00
Env [ ] portainer . Pair
2018-06-11 13:13:19 +00:00
}
func ( payload * updateComposeStackPayload ) Validate ( r * http . Request ) error {
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
}
type updateSwarmStackPayload struct {
2021-02-23 03:21:39 +00:00
// New content of the Stack file
StackFileContent string ` example:"version: 3\n services:\n web:\n image:nginx" `
2021-09-20 00:14:22 +00:00
// A list of environment(endpoint) variables used during stack deployment
2021-02-23 03:21:39 +00:00
Env [ ] portainer . Pair
// Prune services that are no longer referenced (only available for Swarm stacks)
Prune bool ` example:"true" `
2018-06-11 13:13:19 +00:00
}
func ( payload * updateSwarmStackPayload ) Validate ( r * http . Request ) error {
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
}
2021-02-23 03:21:39 +00:00
// @id StackUpdate
// @summary Update a stack
// @description Update a stack.
// @description **Access policy**: restricted
// @tags stacks
// @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-02-23 03:21:39 +00:00
// @param body body updateSwarmStackPayload true "Stack details"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
2021-07-21 23:40:53 +00:00
// @failure 404 "Not found"
2021-02-23 03:21:39 +00:00
// @failure 500 "Server error"
// @router /stacks/{id} [put]
2018-06-11 13:13:19 +00:00
func ( handler * Handler ) stackUpdate ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
2018-06-18 10:07:56 +00:00
stackID , err := request . RetrieveNumericRouteVariableValue ( r , "id" )
2018-06-11 13:13:19 +00:00
if err != nil {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid stack identifier route variable" , err }
}
2020-05-20 05:23:15 +00:00
stack , err := handler . DataStore . Stack ( ) . Stack ( portainer . StackID ( stackID ) )
2020-07-07 21:57:52 +00:00
if err == bolterrors . ErrObjectNotFound {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusNotFound , Message : "Unable to find a stack with the specified identifier inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
} else if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find a stack with the specified identifier inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
}
2018-06-15 15:14:01 +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.
2018-06-15 15:14:01 +00:00
endpointID , err := request . RetrieveNumericQueryParameter ( r , "endpointId" , true )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid query parameter: endpointId" , Err : err }
2018-06-15 15:14:01 +00:00
}
if endpointID != int ( stack . EndpointID ) {
stack . EndpointID = portainer . EndpointID ( endpointID )
}
2020-05-20 05:23:15 +00:00
endpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( stack . EndpointID )
2020-07-07 21:57:52 +00:00
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 }
2018-06-11 13:13:19 +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 }
2018-06-11 13:13:19 +00:00
}
2020-08-11 05:41:37 +00:00
err = handler . requestBouncer . AuthorizedEndpointOperation ( r , endpoint )
2019-05-24 06:04:58 +00:00
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 }
2019-05-24 06:04:58 +00:00
}
securityContext , err := security . RetrieveRestrictedRequestContext ( r )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to retrieve info from request context" , Err : err }
2019-05-24 06:04:58 +00:00
}
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 }
}
2019-05-24 06:04:58 +00:00
}
2018-06-11 13:13:19 +00:00
updateError := handler . updateAndDeployStack ( r , stack , endpoint )
if updateError != nil {
return updateError
}
2021-09-07 00:37:26 +00:00
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
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . UpdateStack ( stack . ID , stack )
2018-06-11 13:13:19 +00:00
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 : err }
2018-06-11 13:13:19 +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 = ""
}
2018-06-11 13:13:19 +00:00
return response . JSON ( w , stack )
}
func ( handler * Handler ) updateAndDeployStack ( r * http . Request , stack * portainer . Stack , endpoint * portainer . Endpoint ) * httperror . HandlerError {
if stack . Type == portainer . DockerSwarmStack {
return handler . updateSwarmStack ( r , stack , endpoint )
2021-09-07 00:37:26 +00:00
} else if stack . Type == portainer . DockerComposeStack {
return handler . updateComposeStack ( r , stack , endpoint )
} else if stack . Type == portainer . KubernetesStack {
return handler . updateKubernetesStack ( r , stack , endpoint )
} else {
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unsupported stack" , Err : errors . Errorf ( "unsupported stack type: %v" , stack . Type ) }
2018-06-11 13:13:19 +00:00
}
}
func ( handler * Handler ) updateComposeStack ( r * http . Request , stack * portainer . Stack , endpoint * portainer . Endpoint ) * httperror . HandlerError {
var payload updateComposeStackPayload
err := request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid request payload" , Err : err }
2018-06-11 13:13:19 +00:00
}
2018-07-12 07:17:07 +00:00
stack . Env = payload . Env
2018-06-20 17:55:00 +00:00
stackFolder := strconv . Itoa ( int ( stack . ID ) )
_ , err = handler . FileService . StoreStackFileFromBytes ( stackFolder , stack . EntryPoint , [ ] byte ( payload . StackFileContent ) )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist updated Compose file on disk" , Err : err }
2018-06-11 13:13:19 +00:00
}
config , configErr := handler . createComposeDeployConfig ( r , stack , endpoint )
if configErr != nil {
return configErr
}
err = handler . deployComposeStack ( config )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
2018-06-11 13:13:19 +00:00
}
return nil
}
func ( handler * Handler ) updateSwarmStack ( r * http . Request , stack * portainer . Stack , endpoint * portainer . Endpoint ) * httperror . HandlerError {
var payload updateSwarmStackPayload
err := request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid request payload" , Err : err }
2018-06-11 13:13:19 +00:00
}
stack . Env = payload . Env
2018-06-20 17:55:00 +00:00
stackFolder := strconv . Itoa ( int ( stack . ID ) )
_ , err = handler . FileService . StoreStackFileFromBytes ( stackFolder , stack . EntryPoint , [ ] byte ( payload . StackFileContent ) )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist updated Compose file on disk" , Err : err }
2018-06-11 13:13:19 +00:00
}
config , configErr := handler . createSwarmDeployConfig ( r , stack , endpoint , payload . Prune )
if configErr != nil {
return configErr
}
err = handler . deploySwarmStack ( config )
if err != nil {
2021-09-07 00:37:26 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
2018-06-11 13:13:19 +00:00
}
return nil
}