Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Refactor template lookup to use strategy pattern #2200

Merged
merged 16 commits into from
Jan 23, 2025
Merged
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()
lindnerby marked this conversation as resolved.
Show resolved Hide resolved
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
Loading