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-02-23 03:21:39 +00:00
// Endpoint identifier of the target endpoint where the stack will be relocated
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 {
2020-07-07 21:57:52 +00:00
return errors . New ( "Invalid endpoint 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
// @summary Migrate a stack to another endpoint
// @description Migrate a stack from an endpoint to another endpoint. It will re-create the stack inside the target endpoint before removing the original stack.
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
// @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 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 {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid stack identifier route variable" , err }
}
var payload stackMigratePayload
err = request . DecodeAndValidateJSONPayload ( r , & payload )
if err != nil {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid request payload" , 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 {
2018-06-19 15:28:40 +00:00
return & httperror . HandlerError { http . StatusNotFound , "Unable to find a stack with the specified identifier inside the database" , err }
} else if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to find a stack with the specified identifier inside the database" , err }
}
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 {
2019-05-24 06:04:58 +00:00
return & httperror . HandlerError { http . StatusNotFound , "Unable to find an endpoint with the specified identifier inside the database" , err }
} else if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to find an endpoint with the specified identifier inside the database" , err }
}
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 {
return & httperror . HandlerError { http . StatusForbidden , "Permission denied to access endpoint" , err }
}
2021-02-23 20:18:05 +00:00
resourceControl , err := handler . DataStore . ResourceControl ( ) . ResourceControlByResourceIDAndType ( stackutils . ResourceControlID ( stack . EndpointID , stack . Name ) , portainer . StackResourceControl )
2019-11-12 23:41:42 +00:00
if err != nil {
2018-06-19 15:28:40 +00:00
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to retrieve a resource control associated to the stack" , err }
}
securityContext , err := security . RetrieveRestrictedRequestContext ( r )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to retrieve info from request context" , err }
}
2019-11-12 23:41:42 +00:00
access , err := handler . userCanAccessStack ( securityContext , endpoint . ID , resourceControl )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to verify user authorizations to validate stack access" , err }
}
if ! access {
2020-07-07 21:57:52 +00:00
return & httperror . HandlerError { http . StatusForbidden , "Access denied to resource" , 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
// 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 {
return & httperror . HandlerError { http . StatusBadRequest , "Invalid query parameter: endpointId" , err }
}
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 {
2018-06-19 15:28:40 +00:00
return & httperror . HandlerError { http . StatusNotFound , "Unable to find an endpoint with the specified identifier inside the database" , err }
} else if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to find an endpoint with the specified identifier inside the database" , err }
}
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-02-23 20:18:05 +00:00
isUnique , err := handler . checkUniqueName ( targetEndpoint , stack . Name , stack . ID , stack . SwarmID != "" )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to check for name collision" , err }
}
if ! isUnique {
errorMessage := fmt . Sprintf ( "A stack with the name '%s' is already running on endpoint '%s'" , stack . Name , targetEndpoint . Name )
return & httperror . HandlerError { http . StatusConflict , errorMessage , errors . New ( errorMessage ) }
}
2018-06-19 15:28:40 +00:00
migrationError := handler . migrateStack ( r , stack , targetEndpoint )
if migrationError != nil {
return migrationError
}
2018-10-01 01:36:49 +00:00
stack . Name = oldName
2018-06-19 15:28:40 +00:00
err = handler . deleteStack ( stack , endpoint )
if err != nil {
return & httperror . HandlerError { http . StatusInternalServerError , err . Error ( ) , err }
}
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 {
return & httperror . HandlerError { http . StatusInternalServerError , "Unable to persist the stack changes inside the database" , err }
}
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 {
return & httperror . HandlerError { http . StatusInternalServerError , err . Error ( ) , err }
}
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 {
return & httperror . HandlerError { http . StatusInternalServerError , err . Error ( ) , err }
}
return nil
}