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" `
// A list of environment variables used during stack deployment
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" `
// A list of environment variables used during stack deployment
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"
// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack."
// @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
// The EndpointID property is not available for these stacks, this API endpoint
// 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 {
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
}