From fb4ffaec3523bd59d6edc3747ac1990ccd57d78c Mon Sep 17 00:00:00 2001
From: Prabhat Khera <91852476+prabhat-portainer@users.noreply.github.com>
Date: Wed, 17 Apr 2024 08:32:32 +1200
Subject: [PATCH] fix(pending-actions): clean pending actions for deleted
 environment [EE-6545] (#11600)

---
 api/dataservices/interface.go                 |  1 +
 .../pendingactions/pendingactions.go          | 31 ++++++++++++++++++
 api/datastore/migrate_data.go                 |  1 +
 .../migrator/migrate_dbversion111.go          | 32 +++++++++++++++++++
 api/datastore/migrator/migrator.go            |  7 ++++
 api/datastore/services_tx.go                  |  4 ++-
 .../test_data/output_24_to_latest.json        |  2 +-
 api/http/handler/endpoints/endpoint_delete.go |  6 ++++
 8 files changed, 82 insertions(+), 2 deletions(-)
 create mode 100644 api/datastore/migrator/migrate_dbversion111.go

diff --git a/api/dataservices/interface.go b/api/dataservices/interface.go
index 22e233ced..4d51fd66a 100644
--- a/api/dataservices/interface.go
+++ b/api/dataservices/interface.go
@@ -73,6 +73,7 @@ type (
 	PendingActionsService interface {
 		BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
 		GetNextIdentifier() int
+		DeleteByEndpointID(ID portainer.EndpointID) error
 	}
 
 	// EdgeStackService represents a service to manage Edge stacks
diff --git a/api/dataservices/pendingactions/pendingactions.go b/api/dataservices/pendingactions/pendingactions.go
index 5b55a8fd9..25b680899 100644
--- a/api/dataservices/pendingactions/pendingactions.go
+++ b/api/dataservices/pendingactions/pendingactions.go
@@ -1,10 +1,12 @@
 package pendingactions
 
 import (
+	"fmt"
 	"time"
 
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/dataservices"
+	"github.com/rs/zerolog/log"
 )
 
 const (
@@ -45,6 +47,12 @@ func (s Service) Update(ID portainer.PendingActionsID, config *portainer.Pending
 	})
 }
 
+func (s Service) DeleteByEndpointID(ID portainer.EndpointID) error {
+	return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
+		return s.Tx(tx).DeleteByEndpointID(ID)
+	})
+}
+
 func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
 	return ServiceTx{
 		BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
@@ -68,6 +76,29 @@ func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.Pendi
 	return s.BaseDataServiceTx.Update(ID, config)
 }
 
+func (s ServiceTx) DeleteByEndpointID(ID portainer.EndpointID) error {
+	log.Debug().Int("endpointId", int(ID)).Msg("deleting pending actions for endpoint")
+	pendingActions, err := s.BaseDataServiceTx.ReadAll()
+	if err != nil {
+		return fmt.Errorf("failed to retrieve pending-actions for endpoint (%d): %w", ID, err)
+	}
+
+	for _, pendingAction := range pendingActions {
+		if pendingAction.EndpointID == ID {
+			err := s.BaseDataServiceTx.Delete(pendingAction.ID)
+			if err != nil {
+				log.Debug().Int("endpointId", int(ID)).Msgf("failed to delete pending action: %v", err)
+			}
+		}
+	}
+	return nil
+}
+
+// GetNextIdentifier returns the next identifier for a custom template.
+func (service ServiceTx) GetNextIdentifier() int {
+	return service.Tx.GetNextIdentifier(BucketName)
+}
+
 // GetNextIdentifier returns the next identifier for a custom template.
 func (service *Service) GetNextIdentifier() int {
 	return service.Connection.GetNextIdentifier(BucketName)
diff --git a/api/datastore/migrate_data.go b/api/datastore/migrate_data.go
index f430051e8..2bbd0db95 100644
--- a/api/datastore/migrate_data.go
+++ b/api/datastore/migrate_data.go
@@ -86,6 +86,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
 		EdgeStackService:        store.EdgeStackService,
 		EdgeJobService:          store.EdgeJobService,
 		TunnelServerService:     store.TunnelServerService,
+		PendingActionsService:   store.PendingActionsService,
 	}
 }
 
diff --git a/api/datastore/migrator/migrate_dbversion111.go b/api/datastore/migrator/migrate_dbversion111.go
new file mode 100644
index 000000000..bd7c0d407
--- /dev/null
+++ b/api/datastore/migrator/migrate_dbversion111.go
@@ -0,0 +1,32 @@
+package migrator
+
+import (
+	portainer "github.com/portainer/portainer/api"
+	"github.com/portainer/portainer/api/dataservices"
+	"github.com/rs/zerolog/log"
+)
+
+func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
+	log.Info().Msg("cleaning up pending actions for deleted endpoints")
+
+	pendingActions, err := migrator.pendingActionsService.ReadAll()
+	if err != nil {
+		return err
+	}
+
+	endpoints := make(map[portainer.EndpointID]struct{})
+	for _, action := range pendingActions {
+		endpoints[action.EndpointID] = struct{}{}
+	}
+
+	for endpointId := range endpoints {
+		_, err := migrator.endpointService.Endpoint(endpointId)
+		if dataservices.IsErrObjectNotFound(err) {
+			err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
diff --git a/api/datastore/migrator/migrator.go b/api/datastore/migrator/migrator.go
index beeb73078..443f8d3a4 100644
--- a/api/datastore/migrator/migrator.go
+++ b/api/datastore/migrator/migrator.go
@@ -14,6 +14,7 @@ import (
 	"github.com/portainer/portainer/api/dataservices/endpointrelation"
 	"github.com/portainer/portainer/api/dataservices/extension"
 	"github.com/portainer/portainer/api/dataservices/fdoprofile"
+	"github.com/portainer/portainer/api/dataservices/pendingactions"
 	"github.com/portainer/portainer/api/dataservices/registry"
 	"github.com/portainer/portainer/api/dataservices/resourcecontrol"
 	"github.com/portainer/portainer/api/dataservices/role"
@@ -58,6 +59,7 @@ type (
 		edgeStackService        *edgestack.Service
 		edgeJobService          *edgejob.Service
 		TunnelServerService     *tunnelserver.Service
+		pendingActionsService   *pendingactions.Service
 	}
 
 	// MigratorParameters represents the required parameters to create a new Migrator instance.
@@ -85,6 +87,7 @@ type (
 		EdgeStackService        *edgestack.Service
 		EdgeJobService          *edgejob.Service
 		TunnelServerService     *tunnelserver.Service
+		PendingActionsService   *pendingactions.Service
 	}
 )
 
@@ -114,6 +117,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
 		edgeStackService:        parameters.EdgeStackService,
 		edgeJobService:          parameters.EdgeJobService,
 		TunnelServerService:     parameters.TunnelServerService,
+		pendingActionsService:   parameters.PendingActionsService,
 	}
 
 	migrator.initMigrations()
@@ -232,6 +236,9 @@ func (m *Migrator) initMigrations() {
 		m.updateAppTemplatesVersionForDB110,
 		m.updateResourceOverCommitToDB110,
 	)
+	m.addMigrations("2.21",
+		m.cleanPendingActionsForDeletedEndpointsForDB111,
+	)
 
 	// Add new migrations below...
 	// One function per migration, each versions migration funcs in the same file.
diff --git a/api/datastore/services_tx.go b/api/datastore/services_tx.go
index a7a40d6e4..0968d18cc 100644
--- a/api/datastore/services_tx.go
+++ b/api/datastore/services_tx.go
@@ -16,7 +16,9 @@ func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
 
 func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
 
-func (tx *StoreTx) PendingActions() dataservices.PendingActionsService { return nil }
+func (tx *StoreTx) PendingActions() dataservices.PendingActionsService {
+	return tx.store.PendingActionsService.Tx(tx.tx)
+}
 
 func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
 	return tx.store.EdgeGroupService.Tx(tx.tx)
diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json
index 59fdcda19..b76e357f3 100644
--- a/api/datastore/test_data/output_24_to_latest.json
+++ b/api/datastore/test_data/output_24_to_latest.json
@@ -940,6 +940,6 @@
     }
   ],
   "version": {
-    "VERSION": "{\"SchemaVersion\":\"2.21.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
+    "VERSION": "{\"SchemaVersion\":\"2.21.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
   }
 }
\ No newline at end of file
diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go
index 5a71faee1..bcfd77ecb 100644
--- a/api/http/handler/endpoints/endpoint_delete.go
+++ b/api/http/handler/endpoints/endpoint_delete.go
@@ -179,6 +179,12 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
 		}
 	}
 
+	// delete the pending actions
+	err = tx.PendingActions().DeleteByEndpointID(endpoint.ID)
+	if err != nil {
+		log.Warn().Err(err).Int("endpointId", int(endpoint.ID)).Msgf("Unable to delete pending actions")
+	}
+
 	err = tx.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID))
 	if err != nil {
 		return httperror.InternalServerError("Unable to delete the environment from the database", err)