2018-06-19 15:28:40 +00:00
package stacks
import (
2020-07-07 21:57:52 +00:00
"errors"
2021-02-23 20:18:05 +00:00
"fmt"
2018-06-19 15:28:40 +00:00
"net/http"
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-02-23 03:21:39 +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"
2019-03-21 01:20:14 +00:00
"github.com/portainer/portainer/api/http/security"
2021-02-23 20:18:05 +00:00
"github.com/portainer/portainer/api/internal/stackutils"
2018-06-19 15:28:40 +00:00
)
type stackMigratePayload struct {
2021-09-20 00:14:22 +00:00
// Environment(Endpoint) identifier of the target environment(endpoint) where the stack will be relocated
2021-02-23 03:21:39 +00:00
EndpointID int ` example:"2" validate:"required" `
// Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated
SwarmID string ` example:"jpofkc0i9uo9wtx1zesuk649w" `
// If provided will rename the migrated stack
Name string ` example:"new-stack" `
2018-06-19 15:28:40 +00:00
}
func ( payload * stackMigratePayload ) Validate ( r * http . Request ) error {
if payload . EndpointID == 0 {
2021-09-08 08:42:17 +00:00
return errors . New ( "Invalid environment identifier. Must be a positive number" )
2018-06-19 15:28:40 +00:00
}
return nil
}
2021-02-23 03:21:39 +00:00
// @id StackMigrate
2021-09-20 00:14:22 +00:00
// @summary Migrate a stack to another environment(endpoint)
// @description Migrate a stack from an environment(endpoint) to another environment(endpoint). It will re-create the stack inside the target environment(endpoint) before removing the original stack.
2021-10-11 23:12:08 +00:00
// @description **Access policy**: authenticated
2021-02-23 03:21:39 +00:00
// @tags stacks
2021-11-30 02:31:16 +00:00
// @security ApiKeyAuth
2021-02-23 03:21:39 +00:00
// @security jwt
// @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 stackMigratePayload true "Stack migration details"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Stack not found"
// @failure 500 "Server error"
// @router /stacks/{id}/migrate [post]
2018-06-19 15:28:40 +00:00
func ( handler * Handler ) stackMigrate ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
stackID , err := request . RetrieveNumericRouteVariableValue ( r , "id" )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid stack identifier route variable" , Err : err }
2018-06-19 15:28:40 +00:00
}
var payload stackMigratePayload
err = request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid request payload" , Err : err }
2018-06-19 15:28:40 +00:00
}
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-29 23:58:10 +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-19 15:28:40 +00:00
} else if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find a stack with the specified identifier inside the database" , Err : err }
}
if stack . Type == portainer . KubernetesStack {
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Migrating a kubernetes stack is not supported" , Err : err }
2018-06-19 15:28:40 +00:00
}
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-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusNotFound , Message : "Unable to find an endpoint with the specified identifier inside the database" , Err : err }
2019-05-24 06:04:58 +00:00
} else if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find an endpoint with the specified identifier inside the database" , Err : err }
2019-05-24 06:04:58 +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-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusForbidden , Message : "Permission denied to access endpoint" , Err : err }
2019-05-24 06:04:58 +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 }
}
2018-06-19 15:28:40 +00:00
2021-09-29 23:58:10 +00:00
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 }
}
2018-06-19 15:28:40 +00:00
2021-09-29 23:58:10 +00:00
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 }
2018-06-19 15:28:40 +00:00
}
2018-06-20 18:02:53 +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-20 18:02:53 +00:00
endpointID , err := request . RetrieveNumericQueryParameter ( r , "endpointId" , true )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid query parameter: endpointId" , Err : err }
2018-06-20 18:02:53 +00:00
}
if endpointID != int ( stack . EndpointID ) {
stack . EndpointID = portainer . EndpointID ( endpointID )
}
2020-05-20 05:23:15 +00:00
targetEndpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( portainer . EndpointID ( payload . EndpointID ) )
2020-07-07 21:57:52 +00:00
if err == bolterrors . ErrObjectNotFound {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusNotFound , Message : "Unable to find an endpoint with the specified identifier inside the database" , Err : err }
2018-06-19 15:28:40 +00:00
} else if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find an endpoint with the specified identifier inside the database" , Err : err }
2018-06-19 15:28:40 +00:00
}
stack . EndpointID = portainer . EndpointID ( payload . EndpointID )
if payload . SwarmID != "" {
stack . SwarmID = payload . SwarmID
}
2018-10-01 01:36:49 +00:00
oldName := stack . Name
if payload . Name != "" {
stack . Name = payload . Name
}
2021-09-29 23:58:10 +00:00
isUnique , err := handler . checkUniqueStackNameInDocker ( targetEndpoint , stack . Name , stack . ID , stack . SwarmID != "" )
2021-02-23 20:18:05 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to check for name collision" , Err : err }
2021-02-23 20:18:05 +00:00
}
if ! isUnique {
2021-09-29 23:58:10 +00:00
errorMessage := fmt . Sprintf ( "A stack with the name '%s' is already running on endpoint '%s'" , stack . Name , targetEndpoint . Name )
return & httperror . HandlerError { StatusCode : http . StatusConflict , Message : errorMessage , Err : errors . New ( errorMessage ) }
2021-02-23 20:18:05 +00:00
}
2018-06-19 15:28:40 +00:00
migrationError := handler . migrateStack ( r , stack , targetEndpoint )
if migrationError != nil {
return migrationError
}
2021-10-12 21:48:28 +00:00
newName := stack . Name
2018-10-01 01:36:49 +00:00
stack . Name = oldName
2021-09-29 23:58:10 +00:00
err = handler . deleteStack ( securityContext . UserID , stack , endpoint )
2018-06-19 15:28:40 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
2018-06-19 15:28:40 +00:00
}
2021-10-12 21:48:28 +00:00
stack . Name = newName
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . UpdateStack ( stack . ID , stack )
2018-06-19 15:28:40 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to persist the stack changes inside the database" , Err : err }
2018-06-19 15:28:40 +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-19 15:28:40 +00:00
return response . JSON ( w , stack )
}
func ( handler * Handler ) migrateStack ( r * http . Request , stack * portainer . Stack , next * portainer . Endpoint ) * httperror . HandlerError {
if stack . Type == portainer . DockerSwarmStack {
return handler . migrateSwarmStack ( r , stack , next )
}
return handler . migrateComposeStack ( r , stack , next )
}
func ( handler * Handler ) migrateComposeStack ( r * http . Request , stack * portainer . Stack , next * portainer . Endpoint ) * httperror . HandlerError {
config , configErr := handler . createComposeDeployConfig ( r , stack , next )
if configErr != nil {
return configErr
}
err := handler . deployComposeStack ( config )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
2018-06-19 15:28:40 +00:00
}
return nil
}
func ( handler * Handler ) migrateSwarmStack ( r * http . Request , stack * portainer . Stack , next * portainer . Endpoint ) * httperror . HandlerError {
config , configErr := handler . createSwarmDeployConfig ( r , stack , next , true )
if configErr != nil {
return configErr
}
err := handler . deploySwarmStack ( config )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : err . Error ( ) , Err : err }
2018-06-19 15:28:40 +00:00
}
return nil
}