Skip to content

Commit

Permalink
Refactor code and algorithm optimization to O(n+m)
Browse files Browse the repository at this point in the history
  • Loading branch information
medmes committed Dec 18, 2024
1 parent cf12cb6 commit 70c1d6b
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 69 deletions.
74 changes: 46 additions & 28 deletions pkg/templatelookup/mandatory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,65 @@ import (
"github.com/kyma-project/lifecycle-manager/api/v1beta2"
)

// GetMandatory returns ModuleTemplates TOs (Transfer Objects) which are marked are mandatory modules.
func GetMandatory(ctx context.Context, kymaClient client.Reader) (ModuleTemplatesByModuleName,
error,
) {
// GetMandatory returns ModuleTemplates TOs (Transfer Objects) which are marked as mandatory modules.
func GetMandatory(ctx context.Context, kymaClient client.Reader) (ModuleTemplatesByModuleName, error) {
mandatoryModuleTemplateList := &v1beta2.ModuleTemplateList{}
labelSelector := k8slabels.SelectorFromSet(k8slabels.Set{shared.IsMandatoryModule: shared.EnableLabelValue})
if err := kymaClient.List(ctx, mandatoryModuleTemplateList,
&client.ListOptions{LabelSelector: labelSelector}); err != nil {

// Fetch mandatory module templates from the Kyma client
if err := kymaClient.List(ctx, mandatoryModuleTemplateList, &client.ListOptions{LabelSelector: labelSelector}); err != nil {
return nil, fmt.Errorf("could not list mandatory ModuleTemplates: %w", err)
}

// maps module name to the module template of the highest version encountered
// Initialize the map to hold the highest versioned template for each module name
mandatoryModules := make(map[string]*ModuleTemplateInfo)
comparator := NewModuleTemplateComparator()

for _, moduleTemplate := range mandatoryModuleTemplateList.Items {
if moduleTemplate.DeletionTimestamp.IsZero() {
currentModuleTemplate := &moduleTemplate
moduleName := GetModuleName(currentModuleTemplate)
if mandatoryModules[moduleName] != nil {
var err error
currentModuleTemplate, err = GetModuleTemplateWithHigherVersion(currentModuleTemplate,
mandatoryModules[moduleName].ModuleTemplate)
if err != nil {
mandatoryModules[moduleName] = &ModuleTemplateInfo{
ModuleTemplate: nil,
Err: err,
}
continue
// Skip deleted modules
if !moduleTemplate.DeletionTimestamp.IsZero() {
continue
}

moduleName := GetModuleName(&moduleTemplate)

// Compare with the existing module in the map (if exists) to find the higher version
if existingModuleTemplateInfo, exists := mandatoryModules[moduleName]; exists {
updatedModuleTemplate, err := comparator.Compare(&moduleTemplate, existingModuleTemplateInfo.ModuleTemplate)
if err != nil {
mandatoryModules[moduleName] = &ModuleTemplateInfo{
ModuleTemplate: nil,
Err: err,
}
continue
}
mandatoryModules[moduleName] = &ModuleTemplateInfo{
ModuleTemplate: updatedModuleTemplate,
Err: nil,
}
} else {
// If the module is encountered for the first time, simply add it
mandatoryModules[moduleName] = &ModuleTemplateInfo{
ModuleTemplate: currentModuleTemplate,
ModuleTemplate: &moduleTemplate,
Err: nil,
}
}
}

return mandatoryModules, nil
}

// GetModuleName returns the name of the module for a given ModuleTemplate.
func GetModuleName(moduleTemplate *v1beta2.ModuleTemplate) string {
if moduleTemplate.Spec.ModuleName != "" {
return moduleTemplate.Spec.ModuleName
}

// https://github.com/kyma-project/lifecycle-manager/issues/2135
// Remove this after warden ModuleTemplate is created using modulectl
// Handle backward compatibility
return moduleTemplate.Labels[shared.ModuleName]
}

// GetModuleSemverVersion returns the semver version of a module template.
func GetModuleSemverVersion(moduleTemplate *v1beta2.ModuleTemplate) (*semver.Version, error) {
if moduleTemplate.Spec.Version != "" {
version, err := semver.NewVersion(moduleTemplate.Spec.Version)
Expand All @@ -70,8 +81,7 @@ func GetModuleSemverVersion(moduleTemplate *v1beta2.ModuleTemplate) (*semver.Ver
return version, nil
}

// https://github.com/kyma-project/lifecycle-manager/issues/2135
// Remove this after warden ModuleTemplate is created using modulectl
// Handle backward compatibility for versions stored in annotations
version, err := semver.NewVersion(moduleTemplate.Annotations[shared.ModuleVersionAnnotation])
if err != nil {
return nil, fmt.Errorf("could not parse version as a semver %s: %w",
Expand All @@ -80,9 +90,16 @@ func GetModuleSemverVersion(moduleTemplate *v1beta2.ModuleTemplate) (*semver.Ver
return version, nil
}

func GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate *v1beta2.ModuleTemplate) (*v1beta2.ModuleTemplate,
error,
) {
// ModuleTemplateComparator helps compare two ModuleTemplates by version.
type ModuleTemplateComparator struct{}

// NewModuleTemplateComparator creates a new instance of ModuleTemplateComparator.
func NewModuleTemplateComparator() *ModuleTemplateComparator {
return &ModuleTemplateComparator{}
}

// Compare compares two module templates and returns the one with the higher version.
func (comparator *ModuleTemplateComparator) Compare(firstModuleTemplate, secondModuleTemplate *v1beta2.ModuleTemplate) (*v1beta2.ModuleTemplate, error) {
firstVersion, err := GetModuleSemverVersion(firstModuleTemplate)
if err != nil {
return nil, err
Expand All @@ -93,6 +110,7 @@ func GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplat
return nil, err
}

// Return the module with the higher version
if firstVersion.GreaterThan(secondVersion) {
return firstModuleTemplate, nil
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/templatelookup/mandatory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"

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

"github.com/kyma-project/lifecycle-manager/pkg/templatelookup"
"github.com/kyma-project/lifecycle-manager/pkg/testutils/builder"
)
Expand All @@ -25,8 +26,9 @@ func TestGetDesiredModuleTemplateForMultipleVersions_ReturnCorrectValue(t *testi
WithLabel("module-diff", "second").
WithAnnotation("operator.kyma-project.io/module-version", "1.0.1-dev").
Build()
moduleTemplateComparator := templatelookup.NewModuleTemplateComparator()

result, err := templatelookup.GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate)
result, err := moduleTemplateComparator.Compare(firstModuleTemplate, secondModuleTemplate)
require.NoError(t, err)
require.Equal(t, secondModuleTemplate, result)
}
Expand All @@ -43,8 +45,9 @@ func TestGetDesiredModuleTemplateForMultipleVersions_ReturnError_NotSemver(t *te
WithVersion("1.0.1-dev").
WithLabel("module-diff", "second").
Build()
moduleTemplateComparator := templatelookup.NewModuleTemplateComparator()

result, err := templatelookup.GetModuleTemplateWithHigherVersion(firstModuleTemplate, secondModuleTemplate)
result, err := moduleTemplateComparator.Compare(firstModuleTemplate, secondModuleTemplate)
require.ErrorContains(t, err, "could not parse version as a semver")
require.Nil(t, result)
}
Expand Down
108 changes: 69 additions & 39 deletions pkg/templatelookup/moduleinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,56 +34,86 @@ func (a ModuleInfo) installedwithVersionInStatus() bool {
return !a.IsEnabled && shared.NoneChannel.Equals(a.Channel) && a.Version != ""
}

// FetchModuleStatusInfo returns a list of [...] objects containing information about modules referenced by the Kyma CR.
// This includes modules that are enabled in `.spec.modules[]` and modules that are not enabled in `.spec.modules[]` but still contain an entry in `.status.modules[]`.
// FetchModuleInfo returns a list of ModuleInfo objects containing information about modules referenced by the Kyma CR.
func FetchModuleInfo(kyma *v1beta2.Kyma) []ModuleInfo {
moduleMap := make(map[string]bool)
modules := make([]ModuleInfo, 0)
for _, module := range kyma.Spec.Modules {
moduleMap[module.Name] = true
if shared.NoneChannel.Equals(module.Channel) {
modules = append(modules, ModuleInfo{
Module: module,
IsEnabled: true,
ValidationError: fmt.Errorf("%w for module %s: Channel \"none\" is not allowed", ErrInvalidModuleInSpec, module.Name),
IsUnmanaged: !module.Managed,
})
continue
}
if module.Version != "" && module.Channel != "" {
modules = append(modules, ModuleInfo{
Module: module,
IsEnabled: true,
ValidationError: fmt.Errorf("%w for module %s: Version and channel are mutually exclusive options", ErrInvalidModuleInSpec, module.Name),
IsUnmanaged: !module.Managed,
})
continue
}
modules = append(modules, ModuleInfo{Module: module, IsEnabled: true, IsUnmanaged: !module.Managed})
moduleMap := buildModuleMap(kyma.Spec.Modules)
specModules := buildModuleInfosFromSpec(kyma.Spec.Modules)
statusModules := buildModuleInfosFromStatus(kyma.Status.Modules, moduleMap)

return append(specModules, statusModules...)
}

// buildModuleMap creates a map for quick lookup of modules from Spec.Modules by name.
func buildModuleMap(modules []v1beta2.Module) map[string]struct{} {
moduleMap := make(map[string]struct{}, len(modules))
for _, module := range modules {
moduleMap[module.Name] = struct{}{}
}
return moduleMap
}

for _, moduleInStatus := range kyma.Status.Modules {
_, exist := moduleMap[moduleInStatus.Name]
if exist {
continue
// buildModuleInfosFromSpec processes Spec.Modules and returns a slice of ModuleInfo.
func buildModuleInfosFromSpec(modules []v1beta2.Module) []ModuleInfo {
moduleInfos := make([]ModuleInfo, 0, len(modules))
for _, module := range modules {
validationError := validateModuleSpec(module)
moduleInfos = append(moduleInfos, newEnabledModuleInfo(module, validationError))
}
return moduleInfos
}

// buildModuleInfosFromStatus processes Status.Modules and returns a slice of ModuleInfo.
func buildModuleInfosFromStatus(
statusModules []v1beta2.ModuleStatus, moduleMap map[string]struct{},
) []ModuleInfo {
moduleInfos := make([]ModuleInfo, 0, len(statusModules))
for _, moduleStatus := range statusModules {
if _, exists := moduleMap[moduleStatus.Name]; !exists {
validationError := determineModuleValidity(moduleStatus)
moduleInfos = append(moduleInfos, newDisabledModuleInfo(moduleStatus, validationError))
}
}
return moduleInfos
}

modules = append(modules, ModuleInfo{
Module: v1beta2.Module{
Name: moduleInStatus.Name,
Channel: moduleInStatus.Channel,
Version: moduleInStatus.Version,
},
IsEnabled: false,
ValidationError: determineModuleValidity(moduleInStatus),
})
// validateModuleSpec validates a module from Spec.Modules and returns an error if invalid.
func validateModuleSpec(module v1beta2.Module) error {
if shared.NoneChannel.Equals(module.Channel) {
return fmt.Errorf("%w for module %s: Channel \"none\" is not allowed", ErrInvalidModuleInSpec, module.Name)
}
if module.Version != "" && module.Channel != "" {
return fmt.Errorf("%w for module %s: Version and channel are mutually exclusive options", ErrInvalidModuleInSpec, module.Name)
}
return modules
return nil
}

// determineModuleValidity validates a module from Status.Modules and returns an error if invalid.
func determineModuleValidity(moduleStatus v1beta2.ModuleStatus) error {
if moduleStatus.Template == nil {
return fmt.Errorf("%w for module %s: ModuleTemplate reference is missing", ErrInvalidModuleInStatus, moduleStatus.Name)
}
return nil
}

// newEnabledModuleInfo creates a ModuleInfo object for enabled modules.
func newEnabledModuleInfo(module v1beta2.Module, validationError error) ModuleInfo {
return ModuleInfo{
Module: module,
IsEnabled: true,
ValidationError: validationError,
IsUnmanaged: !module.Managed,
}
}

// newDisabledModuleInfo creates a ModuleInfo object for disabled modules.
func newDisabledModuleInfo(moduleStatus v1beta2.ModuleStatus, validationError error) ModuleInfo {
return ModuleInfo{
Module: v1beta2.Module{
Name: moduleStatus.Name,
Channel: moduleStatus.Channel,
Version: moduleStatus.Version,
},
IsEnabled: false,
ValidationError: validationError,
}
}

0 comments on commit 70c1d6b

Please sign in to comment.