2018-06-11 13:13:19 +00:00
package stacks
import (
2021-09-06 07:58:26 +00:00
"context"
2021-09-29 23:58:10 +00:00
"fmt"
"io/ioutil"
2018-06-11 13:13:19 +00:00
"net/http"
2021-09-29 23:58:10 +00:00
"os"
2018-06-18 10:07:56 +00:00
"strconv"
2018-06-11 13:13:19 +00:00
2021-09-29 23:58:10 +00:00
"github.com/pkg/errors"
2018-09-10 10:01:38 +00:00
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
2020-05-13 03:37:35 +00:00
portainer "github.com/portainer/portainer/api"
2020-07-07 21:57:52 +00:00
bolterrors "github.com/portainer/portainer/api/bolt/errors"
2021-09-29 23:58:10 +00:00
"github.com/portainer/portainer/api/filesystem"
2020-07-07 21:57:52 +00:00
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
)
2021-02-23 03:21:39 +00:00
// @id StackDelete
// @summary Remove a stack
// @description Remove a stack.
// @description **Access policy**: restricted
// @tags stacks
// @security jwt
// @param id path int true "Stack identifier"
// @param external query boolean false "Set to true to delete an external stack. Only external Swarm stacks are supported"
2021-09-20 00:14:22 +00:00
// @param endpointId query int false "Environment(Endpoint) identifier used to remove an external stack (required when external is set to true)"
2021-02-23 03:21:39 +00:00
// @success 204 "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} [delete]
2018-06-11 13:13:19 +00:00
func ( handler * Handler ) stackDelete ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
stackID , err := request . RetrieveRouteVariableValue ( 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-11 13:13:19 +00:00
}
2020-05-13 03:37:35 +00:00
securityContext , err := security . RetrieveRestrictedRequestContext ( r )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to retrieve info from request context" , Err : err }
2020-05-13 03:37:35 +00:00
}
2018-06-11 13:13:19 +00:00
externalStack , _ := request . RetrieveBooleanQueryParameter ( r , "external" , true )
if externalStack {
2020-05-13 03:37:35 +00:00
return handler . deleteExternalStack ( r , w , stackID , securityContext )
2018-06-11 13:13:19 +00:00
}
2018-06-18 10:07:56 +00:00
id , err := strconv . Atoi ( stackID )
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-18 10:07:56 +00:00
}
2020-05-20 05:23:15 +00:00
stack , err := handler . DataStore . Stack ( ) . Stack ( portainer . StackID ( id ) )
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-11 13:13:19 +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 }
2018-06-11 13:13:19 +00:00
}
2018-06-15 15:14:01 +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-15 15:14:01 +00:00
}
2021-06-10 02:52:33 +00:00
isOrphaned := portainer . EndpointID ( endpointID ) != stack . EndpointID
if isOrphaned && ! securityContext . IsAdmin {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusForbidden , Message : "Permission denied to remove orphaned stack" , Err : errors . New ( "Permission denied to remove orphaned stack" ) }
2018-06-15 15:14:01 +00:00
}
2021-06-10 02:52:33 +00:00
endpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( portainer . EndpointID ( 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 the endpoint associated to the stack inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
} else if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find the endpoint associated to the stack inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
}
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 {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to retrieve a resource control associated to the stack" , Err : err }
2019-05-24 06:04:58 +00:00
}
2021-06-10 02:52:33 +00:00
if ! isOrphaned {
err = handler . requestBouncer . AuthorizedEndpointOperation ( r , endpoint )
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 }
2021-06-10 02:52:33 +00:00
}
2021-09-07 00:37:26 +00:00
if stack . Type == portainer . DockerSwarmStack || stack . Type == portainer . DockerComposeStack {
access , err := handler . userCanAccessStack ( securityContext , endpoint . ID , resourceControl )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to verify user authorizations to validate stack access" , Err : err }
2021-09-07 00:37:26 +00:00
}
if ! access {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusForbidden , Message : "Access denied to resource" , Err : httperrors . ErrResourceAccessDenied }
2021-09-07 00:37:26 +00:00
}
2021-06-10 02:52:33 +00:00
}
2019-05-24 06:04:58 +00:00
}
2021-08-17 01:12:07 +00:00
// stop scheduler updates of the stack before removal
if stack . AutoUpdate != nil {
stopAutoupdate ( stack . ID , stack . AutoUpdate . JobID , * handler . Scheduler )
}
2021-09-29 23:58:10 +00:00
err = handler . deleteStack ( securityContext . UserID , stack , endpoint )
2018-06-11 13:13:19 +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-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
err = handler . DataStore . Stack ( ) . DeleteStack ( portainer . StackID ( id ) )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to remove the stack from the database" , Err : err }
2018-06-11 13:13:19 +00:00
}
2019-11-12 23:41:42 +00:00
if resourceControl != nil {
2020-05-20 05:23:15 +00:00
err = handler . DataStore . ResourceControl ( ) . DeleteResourceControl ( resourceControl . ID )
2019-11-12 23:41:42 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to remove the associated resource control from the database" , Err : err }
2019-11-12 23:41:42 +00:00
}
}
2018-06-11 13:13:19 +00:00
err = handler . FileService . RemoveDirectory ( stack . ProjectPath )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to remove stack files from disk" , Err : err }
2018-06-11 13:13:19 +00:00
}
return response . Empty ( w )
}
2020-05-13 03:37:35 +00:00
func ( handler * Handler ) deleteExternalStack ( r * http . Request , w http . ResponseWriter , stackName string , securityContext * security . RestrictedRequestContext ) * httperror . HandlerError {
endpointID , err := request . RetrieveNumericQueryParameter ( r , "endpointId" , false )
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "Invalid query parameter: endpointId" , Err : err }
2020-05-13 03:37:35 +00:00
}
2020-08-11 05:41:37 +00:00
if ! securityContext . IsAdmin {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusUnauthorized , Message : "Permission denied to delete the stack" , Err : httperrors . ErrUnauthorized }
2020-05-13 03:37:35 +00:00
}
2020-05-20 05:23:15 +00:00
stack , err := handler . DataStore . Stack ( ) . StackByName ( stackName )
2020-07-07 21:57:52 +00:00
if err != nil && err != bolterrors . ErrObjectNotFound {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to check for stack existence inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
}
if stack != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusBadRequest , Message : "A stack with this name exists inside the database. Cannot use external delete method" , Err : errors . New ( "A tag already exists with this name" ) }
2018-06-11 13:13:19 +00:00
}
2020-05-20 05:23:15 +00:00
endpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( portainer . EndpointID ( 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 the endpoint associated to the stack inside the database" , Err : err }
2018-06-11 13:13:19 +00:00
} else if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to find the endpoint 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 )
2018-06-11 13:13:19 +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 }
2018-06-11 13:13:19 +00:00
}
stack = & portainer . Stack {
Name : stackName ,
Type : portainer . DockerSwarmStack ,
}
2021-09-29 23:58:10 +00:00
err = handler . deleteStack ( securityContext . UserID , stack , endpoint )
2018-06-11 13:13:19 +00:00
if err != nil {
2021-09-29 23:58:10 +00:00
return & httperror . HandlerError { StatusCode : http . StatusInternalServerError , Message : "Unable to delete stack" , Err : err }
2018-06-11 13:13:19 +00:00
}
return response . Empty ( w )
}
2021-09-29 23:58:10 +00:00
func ( handler * Handler ) deleteStack ( userID portainer . UserID , stack * portainer . Stack , endpoint * portainer . Endpoint ) error {
2018-06-11 13:13:19 +00:00
if stack . Type == portainer . DockerSwarmStack {
return handler . SwarmStackManager . Remove ( stack , endpoint )
}
2021-09-29 23:58:10 +00:00
if stack . Type == portainer . DockerComposeStack {
return handler . ComposeStackManager . Down ( context . TODO ( ) , stack , endpoint )
}
if stack . Type == portainer . KubernetesStack {
var manifestFiles [ ] string
2021-01-25 19:16:53 +00:00
2021-09-29 23:58:10 +00:00
//if it is a compose format kub stack, create a temp dir and convert the manifest files into it
//then process the remove operation
if stack . IsComposeFormat {
fileNames := append ( [ ] string { stack . EntryPoint } , stack . AdditionalFiles ... )
tmpDir , err := ioutil . TempDir ( "" , "kub_delete" )
if err != nil {
return errors . Wrap ( err , "failed to create temp directory for deleting kub stack" )
}
defer os . RemoveAll ( tmpDir )
for _ , fileName := range fileNames {
2021-11-01 11:01:03 +00:00
manifestFilePath := filesystem . JoinPaths ( tmpDir , fileName )
manifestContent , err := handler . FileService . GetFileContent ( stack . ProjectPath , fileName )
2021-09-29 23:58:10 +00:00
if err != nil {
return errors . Wrap ( err , "failed to read manifest file" )
}
manifestContent , err = handler . KubernetesDeployer . ConvertCompose ( manifestContent )
if err != nil {
return errors . Wrap ( err , "failed to convert docker compose file to a kube manifest" )
}
err = filesystem . WriteToFile ( manifestFilePath , [ ] byte ( manifestContent ) )
if err != nil {
return errors . Wrap ( err , "failed to create temp manifest file" )
}
manifestFiles = append ( manifestFiles , manifestFilePath )
}
} else {
manifestFiles = stackutils . GetStackFilePaths ( stack )
}
out , err := handler . KubernetesDeployer . Remove ( userID , endpoint , manifestFiles , stack . Namespace )
return errors . WithMessagef ( err , "failed to remove kubernetes resources: %q" , out )
}
return fmt . Errorf ( "unsupported stack type: %v" , stack . Type )
2018-06-11 13:13:19 +00:00
}