Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/errors/mandatorymodule/deletion/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package deletion

import "errors"

var (

Check failure on line 5 in internal/errors/mandatorymodule/deletion/errors.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gofumpt)
ErrMrmNotInDeletingState = errors.New("ModuleReleaseMeta not in deleting state")
)
47 changes: 47 additions & 0 deletions internal/service/mandatorymodule/deletion/deletion_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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
}
196 changes: 196 additions & 0 deletions internal/service/mandatorymodule/deletion/deletion_service_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading