diff --git a/internal/errors/mandatorymodule/deletion/errors.go b/internal/errors/mandatorymodule/deletion/errors.go new file mode 100644 index 0000000000..75c5a3f1a4 --- /dev/null +++ b/internal/errors/mandatorymodule/deletion/errors.go @@ -0,0 +1,5 @@ +package deletion + +import "errors" + +var ErrMrmNotInDeletingState = errors.New("ModuleReleaseMeta not in deleting state") diff --git a/internal/service/mandatorymodule/deletion/deletion_service.go b/internal/service/mandatorymodule/deletion/deletion_service.go new file mode 100644 index 0000000000..bae305571d --- /dev/null +++ b/internal/service/mandatorymodule/deletion/deletion_service.go @@ -0,0 +1,48 @@ +package deletion + +import ( + "context" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type UseCase interface { + IsApplicable(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) + Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error +} + +type Service struct { + orderedSteps []UseCase +} + +func NewService(ensureFinalizer UseCase, + skipNonDeleting UseCase, + deleteManifests UseCase, + removeFinalizer UseCase, +) *Service { + return &Service{ + orderedSteps: []UseCase{ + ensureFinalizer, + skipNonDeleting, + deleteManifests, + removeFinalizer, + }, + } +} + +// HandleDeletion processes the deletion of a ModuleReleaseMeta through a series of ordered use cases. +// Returns deletion.ErrMrmNotInDeletingState error if the MRM is not in deleting state, +// which indicates that the controller should not requeue. +func (s *Service) HandleDeletion(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { + // Find the first applicable step and execute it + for _, step := range s.orderedSteps { + isApplicable, err := step.IsApplicable(ctx, mrm) + if err != nil { + return err + } + if isApplicable { + return step.Execute(ctx, mrm) + } + } + return nil +} diff --git a/internal/service/mandatorymodule/deletion/deletion_service_test.go b/internal/service/mandatorymodule/deletion/deletion_service_test.go new file mode 100644 index 0000000000..1e2f7aaa9f --- /dev/null +++ b/internal/service/mandatorymodule/deletion/deletion_service_test.go @@ -0,0 +1,196 @@ +package deletion_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion" +) + +func TestDeletionService_HandleDeletion_ExecutionOrder(t *testing.T) { + t.Parallel() + + var executionOrder []string + + ensureFinalizerStub := &UseCaseStub{UseCaseName: "ensureFinalizer", ExecutionOrder: &executionOrder} + skipNonDeletingStub := &UseCaseStub{UseCaseName: "skipNonDeleting", ExecutionOrder: &executionOrder} + deleteManifestsStub := &UseCaseStub{UseCaseName: "deleteManifests", ExecutionOrder: &executionOrder} + removeFinalizerStub := &UseCaseStub{UseCaseName: "removeFinalizer", ExecutionOrder: &executionOrder} + + service := deletion.NewService( + ensureFinalizerStub, + skipNonDeletingStub, + deleteManifestsStub, + removeFinalizerStub, + ) + mrm := &v1beta2.ModuleReleaseMeta{} + + for range 5 { + err := service.HandleDeletion(context.Background(), mrm) + require.NoError(t, err) + } + + expectedOrder := []string{ + "ensureFinalizer", + "skipNonDeleting", + "deleteManifests", + "removeFinalizer", + } + require.Equal(t, expectedOrder, executionOrder) + + require.True(t, ensureFinalizerStub.IsApplicableCalled) + require.True(t, ensureFinalizerStub.ExecuteCalled) + require.True(t, skipNonDeletingStub.IsApplicableCalled) + require.True(t, skipNonDeletingStub.ExecuteCalled) + require.True(t, deleteManifestsStub.IsApplicableCalled) + require.True(t, deleteManifestsStub.ExecuteCalled) + require.True(t, removeFinalizerStub.IsApplicableCalled) + require.True(t, removeFinalizerStub.ExecuteCalled) +} + +func TestDeletionService_HandleDeletion_ErrorPropagation(t *testing.T) { + t.Parallel() + + var executionOrder []string + + ensureFinalizerErrorStub := &UseCaseErrorStub{ + StubName: "ensureFinalizer", + ExecutionOrder: &executionOrder, + ErrorMessage: "ensureFinalizer failed", + } + skipNonDeletingStub := &UseCaseStub{UseCaseName: "skipNonDeleting", ExecutionOrder: &executionOrder} + deleteManifestsStub := &UseCaseStub{UseCaseName: "deleteManifests", ExecutionOrder: &executionOrder} + removeFinalizerStub := &UseCaseStub{UseCaseName: "removeFinalizer", ExecutionOrder: &executionOrder} + + service := deletion.NewService( + ensureFinalizerErrorStub, + skipNonDeletingStub, + deleteManifestsStub, + removeFinalizerStub, + ) + mrm := &v1beta2.ModuleReleaseMeta{} + + for range 5 { + err := service.HandleDeletion(context.Background(), mrm) + require.Error(t, err) + require.Contains(t, err.Error(), "ensureFinalizer failed") + } + + expectedOrder := []string{ + "ensureFinalizer", + "ensureFinalizer", + "ensureFinalizer", + "ensureFinalizer", + "ensureFinalizer", + } + require.Equal(t, expectedOrder, executionOrder) +} + +func TestDeletionService_HandleDeletion_IsApplicableError(t *testing.T) { + t.Parallel() + + var executionOrder []string + + ensureFinalizerIsApplicableErrorStub := &UseCaseIsApplicableErrorStub{ + StubName: "ensureFinalizer", + ExecutionOrder: &executionOrder, + ErrorMessage: "IsApplicable failed", + } + skipNonDeletingStub := &UseCaseStub{UseCaseName: "skipNonDeleting", ExecutionOrder: &executionOrder} + deleteManifestsStub := &UseCaseStub{UseCaseName: "deleteManifests", ExecutionOrder: &executionOrder} + removeFinalizerStub := &UseCaseStub{UseCaseName: "removeFinalizer", ExecutionOrder: &executionOrder} + + service := deletion.NewService( + ensureFinalizerIsApplicableErrorStub, + skipNonDeletingStub, + deleteManifestsStub, + removeFinalizerStub, + ) + mrm := &v1beta2.ModuleReleaseMeta{} + + err := service.HandleDeletion(context.Background(), mrm) + require.Error(t, err) + require.Contains(t, err.Error(), "IsApplicable failed") + + require.Empty(t, executionOrder) + + require.True(t, ensureFinalizerIsApplicableErrorStub.IsApplicableCalled) + require.False(t, ensureFinalizerIsApplicableErrorStub.ExecuteCalled) + require.False(t, skipNonDeletingStub.IsApplicableCalled) + require.False(t, skipNonDeletingStub.ExecuteCalled) + require.False(t, deleteManifestsStub.IsApplicableCalled) + require.False(t, deleteManifestsStub.ExecuteCalled) + require.False(t, removeFinalizerStub.IsApplicableCalled) + require.False(t, removeFinalizerStub.ExecuteCalled) +} + +// Stubs for the use cases to track execution order and calls + +type UseCaseStub struct { + IsApplicableCalled bool + ExecuteCalled bool + ExecutionOrder *[]string + UseCaseName string +} + +func (stub *UseCaseStub) IsApplicable(_ context.Context, _ *v1beta2.ModuleReleaseMeta) (bool, error) { + if stub.IsApplicableCalled { + return false, nil + } + stub.IsApplicableCalled = true + return true, nil +} + +func (stub *UseCaseStub) Execute(_ context.Context, _ *v1beta2.ModuleReleaseMeta) error { + stub.ExecuteCalled = true + if stub.ExecutionOrder != nil { + *stub.ExecutionOrder = append(*stub.ExecutionOrder, stub.UseCaseName) + } + return nil +} + +type UseCaseErrorStub struct { + IsApplicableCalled bool + ExecuteCalled bool + ExecutionOrder *[]string + StubName string + ErrorMessage string +} + +func (stub *UseCaseErrorStub) IsApplicable(_ context.Context, _ *v1beta2.ModuleReleaseMeta) (bool, error) { + stub.IsApplicableCalled = true + return true, nil +} + +func (stub *UseCaseErrorStub) Execute(_ context.Context, _ *v1beta2.ModuleReleaseMeta) error { + stub.ExecuteCalled = true + if stub.ExecutionOrder != nil { + *stub.ExecutionOrder = append(*stub.ExecutionOrder, stub.StubName) + } + return errors.New(stub.ErrorMessage) +} + +type UseCaseIsApplicableErrorStub struct { + IsApplicableCalled bool + ExecuteCalled bool + ExecutionOrder *[]string + StubName string + ErrorMessage string +} + +func (stub *UseCaseIsApplicableErrorStub) IsApplicable(_ context.Context, _ *v1beta2.ModuleReleaseMeta) (bool, error) { + stub.IsApplicableCalled = true + return false, errors.New(stub.ErrorMessage) +} + +func (stub *UseCaseIsApplicableErrorStub) Execute(_ context.Context, _ *v1beta2.ModuleReleaseMeta) error { + stub.ExecuteCalled = true + if stub.ExecutionOrder != nil { + *stub.ExecutionOrder = append(*stub.ExecutionOrder, stub.StubName) + } + return nil +} diff --git a/internal/service/mandatorymodule/deletion/usecases/delete_manifests.go b/internal/service/mandatorymodule/deletion/usecases/delete_manifests.go new file mode 100644 index 0000000000..e687742c96 --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/delete_manifests.go @@ -0,0 +1,40 @@ +package usecases + +import ( + "context" + "fmt" + + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type ManifestRepo interface { + ListAllForModule(ctx context.Context, moduleName string) ([]apimetav1.PartialObjectMetadata, error) + DeleteAllForModule(ctx context.Context, moduleName string) error +} + +// DeleteManifests is responsible for deleting all manifests associated with a ModuleReleaseMeta. +type DeleteManifests struct { + repo ManifestRepo +} + +func NewDeleteManifests(repo ManifestRepo) *DeleteManifests { + return &DeleteManifests{repo: repo} +} + +// IsApplicable returns true if the ModuleReleaseMeta has associated manifests, so they should be deleted. +func (d *DeleteManifests) IsApplicable(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { + manifests, err := d.repo.ListAllForModule(ctx, mrm.Name) + if err != nil { + return false, fmt.Errorf("failed to list manifests for module %s: %w", mrm.Name, err) + } + return len(manifests) > 0, nil +} + +func (d *DeleteManifests) Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { + if err := d.repo.DeleteAllForModule(ctx, mrm.Name); err != nil { + return fmt.Errorf("failed to delete manifests for module %s: %w", mrm.Name, err) + } + return nil +} diff --git a/internal/service/mandatorymodule/deletion/usecases/delete_manifests_test.go b/internal/service/mandatorymodule/deletion/usecases/delete_manifests_test.go new file mode 100644 index 0000000000..ac6b13b87e --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/delete_manifests_test.go @@ -0,0 +1,120 @@ +package usecases_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion/usecases" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +type MockManifestRepo struct { + ListAllForModuleCalled bool + DeleteAllForModuleCalled bool + ListAllForModuleError error + DeleteAllForModuleError error + CalledWithModuleName string + ManifestsToReturn []apimetav1.PartialObjectMetadata +} + +func (m *MockManifestRepo) ListAllForModule(_ context.Context, moduleName string) ( + []apimetav1.PartialObjectMetadata, error, +) { + m.ListAllForModuleCalled = true + m.CalledWithModuleName = moduleName + return m.ManifestsToReturn, m.ListAllForModuleError +} + +func (m *MockManifestRepo) DeleteAllForModule(_ context.Context, moduleName string) error { + m.DeleteAllForModuleCalled = true + m.CalledWithModuleName = moduleName + return m.DeleteAllForModuleError +} + +func TestDeleteManifests_WithManifests(t *testing.T) { + t.Parallel() + + mockRepo := &MockManifestRepo{ + ManifestsToReturn: []apimetav1.PartialObjectMetadata{ + {ObjectMeta: apimetav1.ObjectMeta{Name: random.Name()}}, + }, + } + deleteManifests := usecases.NewDeleteManifests(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + isApplicable, err := deleteManifests.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.True(t, isApplicable) + require.True(t, mockRepo.ListAllForModuleCalled) + require.Equal(t, mrm.Name, mockRepo.CalledWithModuleName) + + executeErr := deleteManifests.Execute(context.Background(), mrm) + require.NoError(t, executeErr) + require.True(t, mockRepo.DeleteAllForModuleCalled) +} + +func TestDeleteManifests_NoManifests(t *testing.T) { + t.Parallel() + + mockRepo := &MockManifestRepo{ + ManifestsToReturn: []apimetav1.PartialObjectMetadata{}, + } + deleteManifests := usecases.NewDeleteManifests(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + isApplicable, err := deleteManifests.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.False(t, isApplicable) +} + +func TestDeleteManifests_ListError(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("list error") + mockRepo := &MockManifestRepo{ + ListAllForModuleError: expectedErr, + } + deleteManifests := usecases.NewDeleteManifests(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + isApplicable, err := deleteManifests.IsApplicable(context.Background(), mrm) + require.Error(t, err) + require.False(t, isApplicable) + require.Contains(t, err.Error(), "failed to list manifests for module") +} + +func TestDeleteManifests_DeleteError(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("delete error") + mockRepo := &MockManifestRepo{ + DeleteAllForModuleError: expectedErr, + } + deleteManifests := usecases.NewDeleteManifests(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + executeErr := deleteManifests.Execute(context.Background(), mrm) + require.Error(t, executeErr) + require.Contains(t, executeErr.Error(), "failed to delete manifests for module") +} diff --git a/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer.go b/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer.go new file mode 100644 index 0000000000..5b7df59f46 --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer.go @@ -0,0 +1,32 @@ +package usecases + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type MrmEnsureFinalizerRepo interface { + EnsureFinalizer(ctx context.Context, moduleName string, finalizer string) error +} + +// EnsureFinalizer is responsible for ensuring that the mandatory finalizer is present on the ModuleReleaseMeta. +type EnsureFinalizer struct { + repo MrmEnsureFinalizerRepo +} + +func NewEnsureFinalizer(repo MrmEnsureFinalizerRepo) *EnsureFinalizer { + return &EnsureFinalizer{repo: repo} +} + +// IsApplicable returns true if the ModuleReleaseMeta does not contain the mandatory finalizer, so it should be added. +func (e *EnsureFinalizer) IsApplicable(_ context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { + return !controllerutil.ContainsFinalizer(mrm, shared.MandatoryModuleFinalizer), nil +} + +func (e *EnsureFinalizer) Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { + return e.repo.EnsureFinalizer(ctx, mrm.Name, shared.MandatoryModuleFinalizer) +} diff --git a/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer_test.go b/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer_test.go new file mode 100644 index 0000000000..32fa5721ba --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/ensure_finalizer_test.go @@ -0,0 +1,87 @@ +package usecases_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion/usecases" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +type MockMrmEnsureFinalizerRepo struct { + EnsureFinalizerCalled bool + EnsureFinalizerError error + CalledWithModule string + CalledWithFinalizer string +} + +func (m *MockMrmEnsureFinalizerRepo) EnsureFinalizer(_ context.Context, moduleName string, finalizer string) error { + m.EnsureFinalizerCalled = true + m.CalledWithModule = moduleName + m.CalledWithFinalizer = finalizer + return m.EnsureFinalizerError +} + +func TestEnsureFinalizer_WithoutFinalizer(t *testing.T) { + t.Parallel() + + mockRepo := &MockMrmEnsureFinalizerRepo{} + ensureFinalizer := usecases.NewEnsureFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + isApplicable, err := ensureFinalizer.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.True(t, isApplicable) + + executeErr := ensureFinalizer.Execute(context.Background(), mrm) + require.NoError(t, executeErr) + require.True(t, mockRepo.EnsureFinalizerCalled) + require.Equal(t, mrm.Name, mockRepo.CalledWithModule) + require.Equal(t, shared.MandatoryModuleFinalizer, mockRepo.CalledWithFinalizer) +} + +func TestEnsureFinalizer_WithFinalizer(t *testing.T) { + t.Parallel() + + mockRepo := &MockMrmEnsureFinalizerRepo{} + ensureFinalizer := usecases.NewEnsureFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + Finalizers: []string{shared.MandatoryModuleFinalizer}, + }, + } + + isApplicable, err := ensureFinalizer.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.False(t, isApplicable) +} + +func TestEnsureFinalizer_RepositoryError(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("repository error") + mockRepo := &MockMrmEnsureFinalizerRepo{ + EnsureFinalizerError: expectedErr, + } + ensureFinalizer := usecases.NewEnsureFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + executeErr := ensureFinalizer.Execute(context.Background(), mrm) + require.ErrorIs(t, executeErr, expectedErr) + require.True(t, mockRepo.EnsureFinalizerCalled) +} diff --git a/internal/service/mandatorymodule/deletion/usecases/remove_finalizer.go b/internal/service/mandatorymodule/deletion/usecases/remove_finalizer.go new file mode 100644 index 0000000000..a82542dbf7 --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/remove_finalizer.go @@ -0,0 +1,33 @@ +package usecases + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type MrmRemoFinalizerRepo interface { + RemoveFinalizer(ctx context.Context, moduleName string, finalizer string) error +} + +// RemoveFinalizer is responsible for removing the mandatory finalizer from the ModuleReleaseMeta. +type RemoveFinalizer struct { + repo MrmRemoFinalizerRepo +} + +func NewRemoveFinalizer(repo MrmRemoFinalizerRepo) *RemoveFinalizer { + return &RemoveFinalizer{repo: repo} +} + +// IsApplicable returns true if the ModuleReleaseMeta contains the mandatory finalizer, so it should be removed. +// This should be the last step in the deletion process. +func (e *RemoveFinalizer) IsApplicable(_ context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { + return controllerutil.ContainsFinalizer(mrm, shared.MandatoryModuleFinalizer), nil +} + +func (e *RemoveFinalizer) Execute(ctx context.Context, mrm *v1beta2.ModuleReleaseMeta) error { + return e.repo.RemoveFinalizer(ctx, mrm.Name, shared.MandatoryModuleFinalizer) +} diff --git a/internal/service/mandatorymodule/deletion/usecases/remove_finalizer_test.go b/internal/service/mandatorymodule/deletion/usecases/remove_finalizer_test.go new file mode 100644 index 0000000000..e262ba72fc --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/remove_finalizer_test.go @@ -0,0 +1,88 @@ +package usecases_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion/usecases" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +type MockMrmRemoFinalizerRepo struct { + RemoveFinalizerCalled bool + RemoveFinalizerError error + CalledWithModule string + CalledWithFinalizer string +} + +func (m *MockMrmRemoFinalizerRepo) RemoveFinalizer(_ context.Context, moduleName string, finalizer string) error { + m.RemoveFinalizerCalled = true + m.CalledWithModule = moduleName + m.CalledWithFinalizer = finalizer + return m.RemoveFinalizerError +} + +func TestRemoveFinalizer_WithFinalizer(t *testing.T) { + t.Parallel() + + mockRepo := &MockMrmRemoFinalizerRepo{} + removeFinalizer := usecases.NewRemoveFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + Finalizers: []string{shared.MandatoryModuleFinalizer}, + }, + } + + isApplicable, err := removeFinalizer.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.True(t, isApplicable) + + executeErr := removeFinalizer.Execute(context.Background(), mrm) + require.NoError(t, executeErr) + require.True(t, mockRepo.RemoveFinalizerCalled) + require.Equal(t, mrm.Name, mockRepo.CalledWithModule) + require.Equal(t, shared.MandatoryModuleFinalizer, mockRepo.CalledWithFinalizer) +} + +func TestRemoveFinalizer_WithoutFinalizer(t *testing.T) { + t.Parallel() + + mockRepo := &MockMrmRemoFinalizerRepo{} + removeFinalizer := usecases.NewRemoveFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + }, + } + + isApplicable, err := removeFinalizer.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.False(t, isApplicable) +} + +func TestRemoveFinalizer_RepositoryError(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("repository error") + mockRepo := &MockMrmRemoFinalizerRepo{ + RemoveFinalizerError: expectedErr, + } + removeFinalizer := usecases.NewRemoveFinalizer(mockRepo) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: random.Name(), + Finalizers: []string{shared.MandatoryModuleFinalizer}, + }, + } + + executeErr := removeFinalizer.Execute(context.Background(), mrm) + require.ErrorIs(t, executeErr, expectedErr) + require.True(t, mockRepo.RemoveFinalizerCalled) +} diff --git a/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting.go b/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting.go new file mode 100644 index 0000000000..0982a3759a --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting.go @@ -0,0 +1,24 @@ +package usecases + +import ( + "context" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/errors/mandatorymodule/deletion" +) + +// SkipNonDeleting is a use case that skips ModuleReleaseMetas that are not in deleting state. +type SkipNonDeleting struct{} + +func NewSkipNonDeleting() *SkipNonDeleting { + return &SkipNonDeleting{} +} + +// IsApplicable returns true if the ModuleReleaseMeta is not in deleting state, it should be skipped. +func (s *SkipNonDeleting) IsApplicable(_ context.Context, mrm *v1beta2.ModuleReleaseMeta) (bool, error) { + return mrm.DeletionTimestamp.IsZero(), nil +} + +func (s *SkipNonDeleting) Execute(_ context.Context, _ *v1beta2.ModuleReleaseMeta) error { + return deletion.ErrMrmNotInDeletingState +} diff --git a/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting_test.go b/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting_test.go new file mode 100644 index 0000000000..02d0ca0b1b --- /dev/null +++ b/internal/service/mandatorymodule/deletion/usecases/skip_non_deleting_test.go @@ -0,0 +1,49 @@ +package usecases_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/errors/mandatorymodule/deletion" + "github.com/kyma-project/lifecycle-manager/internal/service/mandatorymodule/deletion/usecases" +) + +func TestSkipNonDeleting_NotDeleting(t *testing.T) { + t.Parallel() + + skipNonDeleting := usecases.NewSkipNonDeleting() + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: "test-module", + }, + } + + isApplicable, err := skipNonDeleting.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.True(t, isApplicable) + + executeErr := skipNonDeleting.Execute(context.Background(), mrm) + require.ErrorIs(t, executeErr, deletion.ErrMrmNotInDeletingState) +} + +func TestSkipNonDeleting_IsDeleting(t *testing.T) { + t.Parallel() + + skipNonDeleting := usecases.NewSkipNonDeleting() + now := apimetav1.NewTime(time.Now()) + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: "test-module", + DeletionTimestamp: &now, + }, + } + + isApplicable, err := skipNonDeleting.IsApplicable(context.Background(), mrm) + require.NoError(t, err) + require.False(t, isApplicable) +} diff --git a/unit-test-coverage-lifecycle-manager.yaml b/unit-test-coverage-lifecycle-manager.yaml index 3fa5e36a79..995f1952ca 100644 --- a/unit-test-coverage-lifecycle-manager.yaml +++ b/unit-test-coverage-lifecycle-manager.yaml @@ -43,6 +43,8 @@ packages: internal/service/watcher/gateway: 94 internal/service/watcher/resources: 82 internal/service/skrclient: 35 + internal/service/mandatorymodule/deletion: 100 + internal/service/mandatorymodule/deletion/usecases: 100 internal/util/collections: 87 pkg/module/sync: 10