Skip to content

Commit

Permalink
feat: Refactor template lookup to use strategy pattern (#2200)
Browse files Browse the repository at this point in the history
* feat: Refactor template lookup to use strategy pattern

* fix deletion test

* fix linting

* slim interface signature

* add coverage entry

* adapt expected coverage

* fix e2e tests

* fix deletion test

* add comments to new types

* rename strategies

* add test cases for strategies

---------

Co-authored-by: Benjamin Lindner <[email protected]>
  • Loading branch information
c-pius and lindnerby authored Jan 23, 2025
1 parent 3d85fd0 commit d4ab059
Show file tree
Hide file tree
Showing 28 changed files with 950 additions and 451 deletions.
11 changes: 9 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import (
"github.com/kyma-project/lifecycle-manager/pkg/matcher"
"github.com/kyma-project/lifecycle-manager/pkg/queue"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup"
"github.com/kyma-project/lifecycle-manager/pkg/watcher"

_ "k8s.io/client-go/plugin/pkg/client/auth"
Expand Down Expand Up @@ -280,13 +281,19 @@ func scheduleMetricsCleanup(kymaMetrics *metrics.KymaMetrics, cleanupIntervalInM
func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDescriptorProvider,
skrContextFactory remote.SkrContextProvider, event event.Event, flagVar *flags.FlagVar, options ctrlruntime.Options,
skrWebhookManager *watcher.SKRWebhookManifestManager, kymaMetrics *metrics.KymaMetrics,
setupLog logr.Logger, maintenanceWindow templatelookup.MaintenanceWindow,
setupLog logr.Logger, _ *maintenancewindows.MaintenanceWindow,
) {
options.RateLimiter = internal.RateLimiter(flagVar.FailureBaseDelay,
flagVar.FailureMaxDelay, flagVar.RateLimiterFrequency, flagVar.RateLimiterBurst)
options.CacheSyncTimeout = flagVar.CacheSyncTimeout
options.MaxConcurrentReconciles = flagVar.MaxConcurrentKymaReconciles

moduleTemplateInfoLookupStrategies := moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies([]moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{
moduletemplateinfolookup.NewByVersionStrategy(mgr.GetClient()),
moduletemplateinfolookup.NewByChannelStrategy(mgr.GetClient()),
moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(mgr.GetClient()),
})

if err := (&kyma.Reconciler{
Client: mgr.GetClient(),
SkrContextFactory: skrContextFactory,
Expand All @@ -306,7 +313,7 @@ func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDe
Metrics: kymaMetrics,
RemoteCatalog: remote.NewRemoteCatalogFromKyma(mgr.GetClient(), skrContextFactory,
flagVar.RemoteSyncNamespace),
TemplateLookup: templatelookup.NewTemplateLookup(mgr.GetClient(), descriptorProvider, maintenanceWindow),
TemplateLookup: templatelookup.NewTemplateLookup(mgr.GetClient(), descriptorProvider, moduleTemplateInfoLookupStrategies),
}).SetupWithManager(
mgr, options, kyma.SetupOptions{
ListenerAddr: flagVar.KymaListenerAddr,
Expand Down
3 changes: 2 additions & 1 deletion pkg/module/sync/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/kyma-project/lifecycle-manager/pkg/log"
"github.com/kyma-project/lifecycle-manager/pkg/module/common"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup"
"github.com/kyma-project/lifecycle-manager/pkg/util"
)

Expand Down Expand Up @@ -275,7 +276,7 @@ func generateModuleStatus(module *common.Module, existStatus *v1beta2.ModuleStat
newModuleStatus.Message = module.Template.Err.Error()
return *newModuleStatus
}
if errors.Is(module.Template.Err, templatelookup.ErrNoTemplatesInListResult) {
if errors.Is(module.Template.Err, moduletemplateinfolookup.ErrNoTemplatesInListResult) {
return v1beta2.ModuleStatus{
Name: module.ModuleName,
Channel: module.Template.DesiredChannel,
Expand Down
135 changes: 135 additions & 0 deletions pkg/templatelookup/moduletemplateinfolookup/by_channel_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package moduletemplateinfolookup

import (
"context"
"errors"
"fmt"

"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"

"github.com/kyma-project/lifecycle-manager/api/v1beta2"
"github.com/kyma-project/lifecycle-manager/pkg/log"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup"
)

var ErrNotDefaultChannelAllowed = errors.New("specifying no default channel is not allowed")

// ByChannelStrategy looks up the module template for a given channel-based installation.
type ByChannelStrategy struct {
client client.Reader
}

func NewByChannelStrategy(client client.Reader) ByChannelStrategy {
return ByChannelStrategy{client: client}
}

func (ByChannelStrategy) IsResponsible(moduleInfo *templatelookup.ModuleInfo, moduleReleaseMeta *v1beta2.ModuleReleaseMeta) bool {
if moduleReleaseMeta != nil {
return false
}

if moduleInfo.IsInstalledByVersion() {
return false
}

return true
}

func (s ByChannelStrategy) Lookup(ctx context.Context,
moduleInfo *templatelookup.ModuleInfo,
kyma *v1beta2.Kyma,
_ *v1beta2.ModuleReleaseMeta,
) templatelookup.ModuleTemplateInfo {
desiredChannel := getDesiredChannel(moduleInfo.Channel, kyma.Spec.Channel)
info := templatelookup.ModuleTemplateInfo{
DesiredChannel: desiredChannel,
}

template, err := s.filterTemplatesByChannel(ctx, moduleInfo.Name, desiredChannel)
if err != nil {
info.Err = err
return info
}

actualChannel := template.Spec.Channel
if actualChannel == "" {
info.Err = fmt.Errorf(
"no channel found on template for module: %s: %w",
moduleInfo.Name, ErrNotDefaultChannelAllowed,
)
return info
}

logUsedChannel(ctx, moduleInfo.Name, actualChannel, kyma.Spec.Channel)
info.ModuleTemplate = template
return info
}

func (s ByChannelStrategy) filterTemplatesByChannel(ctx context.Context, name, desiredChannel string) (
*v1beta2.ModuleTemplate, error,
) {
templateList := &v1beta2.ModuleTemplateList{}
err := s.client.List(ctx, templateList)
if err != nil {
return nil, fmt.Errorf("failed to list module templates on lookup: %w", err)
}

var filteredTemplates []*v1beta2.ModuleTemplate
for _, template := range templateList.Items {
if TemplateNameMatch(&template, name) && template.Spec.Channel == desiredChannel {
filteredTemplates = append(filteredTemplates, &template)
continue
}
}

if len(filteredTemplates) > 1 {
return nil, newMoreThanOneTemplateCandidateErr(name, templateList.Items)
}

if len(filteredTemplates) == 0 {
return nil, fmt.Errorf("%w: for module %s in channel %s ",
ErrNoTemplatesInListResult, name, desiredChannel)
}

if filteredTemplates[0].Spec.Mandatory {
return nil, fmt.Errorf("%w: for module %s in channel %s",
ErrTemplateMarkedAsMandatory, name, desiredChannel)
}

return filteredTemplates[0], nil
}

func getDesiredChannel(moduleChannel, globalChannel string) string {
var desiredChannel string

switch {
case moduleChannel != "":
desiredChannel = moduleChannel
case globalChannel != "":
desiredChannel = globalChannel
default:
desiredChannel = v1beta2.DefaultChannel
}

return desiredChannel
}

func logUsedChannel(ctx context.Context, name string, actualChannel string, defaultChannel string) {
logger := logf.FromContext(ctx)
if actualChannel != defaultChannel {
logger.V(log.DebugLevel).Info(
fmt.Sprintf(
"using %s (instead of %s) for module %s",
actualChannel, defaultChannel, name,
),
)
} else {
logger.V(log.DebugLevel).Info(
fmt.Sprintf(
"using %s for module %s",
actualChannel, name,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package moduletemplateinfolookup_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

"github.com/kyma-project/lifecycle-manager/api/v1beta2"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup"
"github.com/kyma-project/lifecycle-manager/pkg/testutils/builder"
)

func Test_ByChannelStrategy_IsResponsible_ReturnsTrue(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithChannel("regular").Enabled().Build()
var moduleReleaseMeta *v1beta2.ModuleReleaseMeta = nil
byChannelStrategy := moduletemplateinfolookup.NewByChannelStrategy(nil)

responsible := byChannelStrategy.IsResponsible(moduleInfo, moduleReleaseMeta)

assert.True(t, responsible)
}

func Test_ByChannelStrategy_IsResponsible_ReturnsFalse_WhenModuleReleaseMetaIsNotNil(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithChannel("regular").Enabled().Build()
moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder().Build()
byChannelStrategy := moduletemplateinfolookup.NewByChannelStrategy(nil)

responsible := byChannelStrategy.IsResponsible(moduleInfo, moduleReleaseMeta)

assert.False(t, responsible)
}

func Test_ByChannelStrategy_IsResponsible_ReturnsFalse_WhenInstalledByVersion(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithVersion("1.0.0").Enabled().Build()
var moduleReleaseMeta *v1beta2.ModuleReleaseMeta = nil
byChannelStrategy := moduletemplateinfolookup.NewByChannelStrategy(nil)

responsible := byChannelStrategy.IsResponsible(moduleInfo, moduleReleaseMeta)

assert.False(t, responsible)
}

func Test_ByChannelStrategy_Lookup_ReturnsModuleTemplateInfo(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithName("test-module").WithChannel("regular").Enabled().Build()
kyma := builder.NewKymaBuilder().Build()
var moduleReleaseMeta *v1beta2.ModuleReleaseMeta = nil
moduleTemplate := builder.NewModuleTemplateBuilder().
WithName("test-module-regular").
WithModuleName("test-module").
WithVersion("").
WithChannel("regular").
Build()
byChannelStrategy := moduletemplateinfolookup.NewByChannelStrategy(fakeClient(
&v1beta2.ModuleTemplateList{
Items: []v1beta2.ModuleTemplate{
*moduleTemplate,
},
},
))

moduleTemplateInfo := byChannelStrategy.Lookup(context.Background(), moduleInfo, kyma, moduleReleaseMeta)

assert.NotNil(t, moduleTemplateInfo)
assert.Equal(t, moduleTemplate.Name, moduleTemplateInfo.ModuleTemplate.Name)
assert.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.ModuleTemplate.Spec.ModuleName)
assert.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.ModuleTemplate.Spec.Version)
assert.Equal(t, moduleTemplate.Spec.Channel, moduleTemplateInfo.ModuleTemplate.Spec.Channel)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package moduletemplateinfolookup

import (
"context"

"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kyma-project/lifecycle-manager/api/v1beta2"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup"
)

// ByModuleReleaseMetaStrategy looks up the module template via the module release meta.
// It only supports channel-based installation.
type ByModuleReleaseMetaStrategy struct {
client client.Reader
}

func NewByModuleReleaseMetaStrategy(client client.Reader) ByModuleReleaseMetaStrategy {
return ByModuleReleaseMetaStrategy{client: client}
}

func (ByModuleReleaseMetaStrategy) IsResponsible(_ *templatelookup.ModuleInfo, moduleReleaseMeta *v1beta2.ModuleReleaseMeta) bool {
return moduleReleaseMeta != nil
}

func (s ByModuleReleaseMetaStrategy) Lookup(ctx context.Context,
moduleInfo *templatelookup.ModuleInfo,
kyma *v1beta2.Kyma,
moduleReleaseMeta *v1beta2.ModuleReleaseMeta,
) templatelookup.ModuleTemplateInfo {
moduleTemplateInfo := templatelookup.ModuleTemplateInfo{}

moduleTemplateInfo.DesiredChannel = getDesiredChannel(moduleInfo.Channel, kyma.Spec.Channel)
desiredModuleVersion, err := templatelookup.GetChannelVersionForModule(moduleReleaseMeta, moduleTemplateInfo.DesiredChannel)
if err != nil {
moduleTemplateInfo.Err = err
return moduleTemplateInfo
}

template, err := getTemplateByVersion(ctx,
s.client,
moduleInfo.Name,
desiredModuleVersion,
kyma.Namespace)
if err != nil {
moduleTemplateInfo.Err = err
return moduleTemplateInfo
}

moduleTemplateInfo.ModuleTemplate = template
return moduleTemplateInfo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package moduletemplateinfolookup_test

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
machineryruntime "k8s.io/apimachinery/pkg/runtime"
machineryutilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/kyma-project/lifecycle-manager/api"
"github.com/kyma-project/lifecycle-manager/api/v1beta2"
"github.com/kyma-project/lifecycle-manager/pkg/templatelookup/moduletemplateinfolookup"
"github.com/kyma-project/lifecycle-manager/pkg/testutils/builder"
)

func Test_ByModuleReleaseMetaStrategy_IsResponsible_ReturnsTrue(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithChannel("regular").Enabled().Build()
moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder().Build()
byMRMStrategy := moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(nil)

responsible := byMRMStrategy.IsResponsible(moduleInfo, moduleReleaseMeta)

assert.True(t, responsible)
}

func Test_ByModuleReleaseMetaStrategy_IsResponsible_ReturnsFalse_WhenModuleReleaseMetaIsNotNil(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithVersion("regular").Enabled().Build()
var moduleReleaseMeta *v1beta2.ModuleReleaseMeta = nil
byMRMStrategy := moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(nil)

responsible := byMRMStrategy.IsResponsible(moduleInfo, moduleReleaseMeta)

assert.False(t, responsible)
}

func Test_ByModuleReleaseMeta_Strategy_Lookup_ReturnsModuleTemplateInfo(t *testing.T) {
moduleInfo := newModuleInfoBuilder().WithName("test-module").WithChannel("regular").Enabled().Build()
kyma := builder.NewKymaBuilder().Build()
moduleReleaseMeta := builder.NewModuleReleaseMetaBuilder().
WithModuleName("test-module").
WithName("test-module").
WithModuleChannelAndVersions([]v1beta2.ChannelVersionAssignment{
{
Channel: "regular",
Version: "1.0.0",
},
}).
Build()
moduleTemplate := builder.NewModuleTemplateBuilder().
WithName("test-module-1.0.0").
WithModuleName("test-module").
WithVersion("1.0.0").
WithChannel("none").
Build()
byMRMStrategy := moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(fakeClient(
&v1beta2.ModuleTemplateList{
Items: []v1beta2.ModuleTemplate{
*moduleTemplate,
},
},
))

moduleTemplateInfo := byMRMStrategy.Lookup(context.Background(), moduleInfo, kyma, moduleReleaseMeta)

assert.NotNil(t, moduleTemplateInfo)
assert.Equal(t, moduleTemplate.Name, moduleTemplateInfo.ModuleTemplate.Name)
assert.Equal(t, moduleTemplate.Spec.ModuleName, moduleTemplateInfo.ModuleTemplate.Spec.ModuleName)
assert.Equal(t, moduleTemplate.Spec.Version, moduleTemplateInfo.ModuleTemplate.Spec.Version)
assert.Equal(t, moduleTemplate.Spec.Channel, moduleTemplateInfo.ModuleTemplate.Spec.Channel)
}

func fakeClient(mts *v1beta2.ModuleTemplateList) client.Client {
scheme := machineryruntime.NewScheme()
machineryutilruntime.Must(api.AddToScheme(scheme))

return fake.NewClientBuilder().WithScheme(scheme).WithLists(mts).Build()
}
Loading

0 comments on commit d4ab059

Please sign in to comment.