diff --git a/cmd/main.go b/cmd/main.go index 89f56799e6..d2b10cfada 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -64,6 +64,7 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/log" "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/watcher" _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -192,15 +193,16 @@ func setupManager(flagVar *flags.FlagVar, cacheOptions cache.Options, scheme *ma kymaMetrics := metrics.NewKymaMetrics(sharedMetrics) mandatoryModulesMetrics := metrics.NewMandatoryModulesMetrics() - // The maintenance windows policy should be passed to the reconciler to be resolved: https://github.com/kyma-project/lifecycle-manager/issues/2101 - _, err = maintenancewindows.InitializeMaintenanceWindowsPolicy(setupLog, maintenanceWindowPoliciesDirectory, + maintenanceWindow, err := maintenancewindows.InitializeMaintenanceWindow(setupLog, + maintenanceWindowPoliciesDirectory, maintenanceWindowPolicyName) if err != nil { setupLog.Error(err, "unable to set maintenance windows policy") } setupKymaReconciler(mgr, descriptorProvider, skrContextProvider, eventRecorder, flagVar, options, skrWebhookManager, - kymaMetrics, setupLog) - setupManifestReconciler(mgr, flagVar, options, sharedMetrics, mandatoryModulesMetrics, setupLog, eventRecorder) + kymaMetrics, setupLog, maintenanceWindow) + setupManifestReconciler(mgr, flagVar, options, sharedMetrics, mandatoryModulesMetrics, setupLog, + eventRecorder) setupMandatoryModuleReconciler(mgr, descriptorProvider, flagVar, options, mandatoryModulesMetrics, setupLog) setupMandatoryModuleDeletionReconciler(mgr, descriptorProvider, eventRecorder, flagVar, options, setupLog) if flagVar.EnablePurgeFinalizer { @@ -277,7 +279,8 @@ 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, + skrWebhookManager *watcher.SKRWebhookManifestManager, kymaMetrics *metrics.KymaMetrics, + setupLog logr.Logger, maintenanceWindow templatelookup.MaintenanceWindow, ) { options.RateLimiter = internal.RateLimiter(flagVar.FailureBaseDelay, flagVar.FailureMaxDelay, flagVar.RateLimiterFrequency, flagVar.RateLimiterBurst) @@ -303,6 +306,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), }).SetupWithManager( mgr, options, kyma.SetupOptions{ ListenerAddr: flagVar.KymaListenerAddr, diff --git a/internal/controller/kyma/controller.go b/internal/controller/kyma/controller.go index c81ae9d7b9..1b70afcc2b 100644 --- a/internal/controller/kyma/controller.go +++ b/internal/controller/kyma/controller.go @@ -72,6 +72,7 @@ type Reconciler struct { IsManagedKyma bool Metrics *metrics.KymaMetrics RemoteCatalog *remote.RemoteCatalog + TemplateLookup *templatelookup.TemplateLookup } // +kubebuilder:rbac:groups=operator.kyma-project.io,resources=kymas,verbs=get;list;watch;create;update;patch;delete @@ -504,7 +505,7 @@ func (r *Reconciler) updateKyma(ctx context.Context, kyma *v1beta2.Kyma) error { } func (r *Reconciler) reconcileManifests(ctx context.Context, kyma *v1beta2.Kyma) error { - templates := templatelookup.NewTemplateLookup(client.Reader(r), r.DescriptorProvider).GetRegularTemplates(ctx, kyma) + templates := r.TemplateLookup.GetRegularTemplates(ctx, kyma) prsr := parser.NewParser(r.Client, r.DescriptorProvider, r.InKCPMode, r.RemoteSyncNamespace) modules := prsr.GenerateModulesFromTemplates(kyma, templates) diff --git a/internal/maintenancewindows/maintenance_policy_handler.go b/internal/maintenancewindows/maintenance_policy_handler.go deleted file mode 100644 index 64d004922b..0000000000 --- a/internal/maintenancewindows/maintenance_policy_handler.go +++ /dev/null @@ -1,44 +0,0 @@ -package maintenancewindows - -import ( - "fmt" - "os" - - "github.com/go-logr/logr" - - "github.com/kyma-project/lifecycle-manager/maintenancewindows/resolver" -) - -func InitializeMaintenanceWindowsPolicy(log logr.Logger, - policiesDirectory, policyName string, -) (*resolver.MaintenanceWindowPolicy, error) { - if err := os.Setenv(resolver.PolicyPathENV, policiesDirectory); err != nil { - return nil, fmt.Errorf("failed to set the policy path env variable, %w", err) - } - - policyFilePath := fmt.Sprintf("%s/%s.json", policiesDirectory, policyName) - if !MaintenancePolicyFileExists(policyFilePath) { - log.Info("maintenance windows policy file does not exist") - return nil, nil //nolint:nilnil //use nil to indicate an empty Maintenance Window Policy - } - - maintenancePolicyPool, err := resolver.GetMaintenancePolicyPool() - if err != nil { - return nil, fmt.Errorf("failed to get maintenance policy pool, %w", err) - } - - maintenancePolicy, err := resolver.GetMaintenancePolicy(maintenancePolicyPool, policyName) - if err != nil { - return nil, fmt.Errorf("failed to get maintenance window policy, %w", err) - } - - return maintenancePolicy, nil -} - -func MaintenancePolicyFileExists(policyFilePath string) bool { - if _, err := os.Stat(policyFilePath); os.IsNotExist(err) { - return false - } - - return true -} diff --git a/internal/maintenancewindows/maintenance_policy_handler_test.go b/internal/maintenancewindows/maintenance_policy_handler_test.go deleted file mode 100644 index 2eec61ace0..0000000000 --- a/internal/maintenancewindows/maintenance_policy_handler_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package maintenancewindows_test - -import ( - "fmt" - "testing" - "time" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - - "github.com/kyma-project/lifecycle-manager/internal/maintenancewindows" - "github.com/kyma-project/lifecycle-manager/maintenancewindows/resolver" -) - -func TestMaintenancePolicyFileExists_FileNotExists(t *testing.T) { - got := maintenancewindows.MaintenancePolicyFileExists("testdata/file.json") - - require.False(t, got) -} - -func TestMaintenancePolicyFileExists_FileExists(t *testing.T) { - got := maintenancewindows.MaintenancePolicyFileExists("testdata/policy.json") - - require.True(t, got) -} - -func TestInitializeMaintenanceWindowsPolicy_FileNotExist_NoError(t *testing.T) { - got, err := maintenancewindows.InitializeMaintenanceWindowsPolicy(logr.Logger{}, "testdata", "policy-1") - - require.Nil(t, got) - require.NoError(t, err) -} - -func TestInitializeMaintenanceWindowsPolicy_DirectoryNotExist_NoError(t *testing.T) { - got, err := maintenancewindows.InitializeMaintenanceWindowsPolicy(logr.Logger{}, "files", "policy") - - require.Nil(t, got) - require.NoError(t, err) -} - -func TestInitializeMaintenanceWindowsPolicy_InvalidPolicy(t *testing.T) { - got, err := maintenancewindows.InitializeMaintenanceWindowsPolicy(logr.Logger{}, "testdata", "invalid-policy") - - require.Nil(t, got) - require.ErrorContains(t, err, "failed to get maintenance window policy") -} - -func TestInitializeMaintenanceWindowsPolicy_WhenFileExists_CorrectPolicyIsRead(t *testing.T) { - got, err := maintenancewindows.InitializeMaintenanceWindowsPolicy(logr.Logger{}, "testdata", "policy") - require.NoError(t, err) - - ruleOneBeginTime, err := parseTime("01:00:00+00:00") - require.NoError(t, err) - ruleOneEndTime, err := parseTime("01:00:00+00:00") - require.NoError(t, err) - - ruleTwoBeginTime, err := parseTime("21:00:00+00:00") - require.NoError(t, err) - ruleTwoEndTime, err := parseTime("00:00:00+00:00") - require.NoError(t, err) - - defaultBeginTime, err := parseTime("21:00:00+00:00") - require.NoError(t, err) - defaultEndTime, err := parseTime("23:00:00+00:00") - require.NoError(t, err) - - expectedPolicy := &resolver.MaintenanceWindowPolicy{ - Rules: []resolver.MaintenancePolicyRule{ - { - Match: resolver.MaintenancePolicyMatch{ - Plan: resolver.NewRegexp("trial|free"), - }, - Windows: resolver.MaintenanceWindows{ - { - Days: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}, - Begin: resolver.WindowTime(ruleOneBeginTime), - End: resolver.WindowTime(ruleOneEndTime), - }, - }, - }, - { - Match: resolver.MaintenancePolicyMatch{ - Region: resolver.NewRegexp("europe|eu-|uksouth"), - }, - Windows: resolver.MaintenanceWindows{ - { - Days: []string{"Sat"}, - Begin: resolver.WindowTime(ruleTwoBeginTime), - End: resolver.WindowTime(ruleTwoEndTime), - }, - }, - }, - }, - Default: resolver.MaintenanceWindow{ - Days: []string{"Sat"}, - Begin: resolver.WindowTime(defaultBeginTime), - End: resolver.WindowTime(defaultEndTime), - }, - } - - require.NoError(t, err) - require.Equal(t, expectedPolicy, got) -} - -func parseTime(value string) (time.Time, error) { - t, err := time.Parse("15:04:05Z07:00", value) - if err != nil { - return time.Time{}, fmt.Errorf("failed to parse time: %w", err) - } - - return t, nil -} diff --git a/internal/maintenancewindows/maintenance_window.go b/internal/maintenancewindows/maintenance_window.go new file mode 100644 index 0000000000..2f1a5389f4 --- /dev/null +++ b/internal/maintenancewindows/maintenance_window.go @@ -0,0 +1,110 @@ +package maintenancewindows + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/go-logr/logr" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/maintenancewindows/resolver" +) + +var ErrNoMaintenanceWindowPolicyConfigured = errors.New("no maintenance window policy configured") + +type MaintenanceWindowPolicy interface { + Resolve(runtime *resolver.Runtime, opts ...interface{}) (*resolver.ResolvedWindow, error) +} + +type MaintenanceWindow struct { + // make this private once we refactor the API + // https://github.com/kyma-project/lifecycle-manager/issues/2190 + MaintenanceWindowPolicy MaintenanceWindowPolicy +} + +func InitializeMaintenanceWindow(log logr.Logger, + policiesDirectory, policyName string, +) (*MaintenanceWindow, error) { + if err := os.Setenv(resolver.PolicyPathENV, policiesDirectory); err != nil { + return nil, fmt.Errorf("failed to set the policy path env variable, %w", err) + } + + policyFilePath := fmt.Sprintf("%s/%s.json", policiesDirectory, policyName) + if !MaintenancePolicyFileExists(policyFilePath) { + log.Info("maintenance windows policy file does not exist") + return &MaintenanceWindow{ + MaintenanceWindowPolicy: nil, + }, nil + } + + maintenancePolicyPool, err := resolver.GetMaintenancePolicyPool() + if err != nil { + return nil, fmt.Errorf("failed to get maintenance policy pool, %w", err) + } + + maintenancePolicy, err := resolver.GetMaintenancePolicy(maintenancePolicyPool, policyName) + if err != nil { + return nil, fmt.Errorf("failed to get maintenance window policy, %w", err) + } + + return &MaintenanceWindow{ + MaintenanceWindowPolicy: maintenancePolicy, + }, nil +} + +func MaintenancePolicyFileExists(policyFilePath string) bool { + if _, err := os.Stat(policyFilePath); os.IsNotExist(err) { + return false + } + + return true +} + +// IsRequired determines if a maintenance window is required to update the given module. +func (MaintenanceWindow) IsRequired(moduleTemplate *v1beta2.ModuleTemplate, kyma *v1beta2.Kyma) bool { + if !moduleTemplate.Spec.RequiresDowntime { + return false + } + + if kyma.Spec.SkipMaintenanceWindows { + return false + } + + // module not installed yet => no need for maintenance window + moduleStatus := kyma.Status.GetModuleStatus(moduleTemplate.Spec.ModuleName) + if moduleStatus == nil { + return false + } + + // module already installed in this version => no need for maintenance window + installedVersion := moduleStatus.Version + return installedVersion != moduleTemplate.Spec.Version +} + +// IsActive determines if a maintenance window is currently active. +func (mw MaintenanceWindow) IsActive(kyma *v1beta2.Kyma) (bool, error) { + if mw.MaintenanceWindowPolicy == nil { + return false, ErrNoMaintenanceWindowPolicyConfigured + } + + runtime := &resolver.Runtime{ + GlobalAccountID: kyma.GetGlobalAccount(), + Region: kyma.GetRegion(), + PlatformRegion: kyma.GetPlatformRegion(), + Plan: kyma.GetPlan(), + } + + resolvedWindow, err := mw.MaintenanceWindowPolicy.Resolve(runtime) + if err != nil { + return false, err + } + + now := time.Now() + if now.After(resolvedWindow.Begin) && now.Before(resolvedWindow.End) { + return true, nil + } + + return false, nil +} diff --git a/internal/maintenancewindows/maintenance_window_test.go b/internal/maintenancewindows/maintenance_window_test.go new file mode 100644 index 0000000000..299fe33fd8 --- /dev/null +++ b/internal/maintenancewindows/maintenance_window_test.go @@ -0,0 +1,338 @@ +package maintenancewindows_test + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/maintenancewindows" + "github.com/kyma-project/lifecycle-manager/maintenancewindows/resolver" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/builder" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +func TestMaintenancePolicyFileExists_FileNotExists(t *testing.T) { + got := maintenancewindows.MaintenancePolicyFileExists("testdata/file.json") + + require.False(t, got) +} + +func TestMaintenancePolicyFileExists_FileExists(t *testing.T) { + got := maintenancewindows.MaintenancePolicyFileExists("testdata/policy.json") + + require.True(t, got) +} + +func TestInitializeMaintenanceWindowsPolicy_FileNotExist_NoError(t *testing.T) { + got, err := maintenancewindows.InitializeMaintenanceWindow(logr.Logger{}, "testdata", "policy-1") + + require.Nil(t, got.MaintenanceWindowPolicy) + require.NoError(t, err) +} + +func TestInitializeMaintenanceWindowsPolicy_DirectoryNotExist_NoError(t *testing.T) { + got, err := maintenancewindows.InitializeMaintenanceWindow(logr.Logger{}, "files", "policy") + + require.Nil(t, got.MaintenanceWindowPolicy) + require.NoError(t, err) +} + +func TestInitializeMaintenanceWindowsPolicy_InvalidPolicy(t *testing.T) { + got, err := maintenancewindows.InitializeMaintenanceWindow(logr.Logger{}, "testdata", "invalid-policy") + + require.Nil(t, got) + require.ErrorContains(t, err, "failed to get maintenance window policy") +} + +func TestInitializeMaintenanceWindowsPolicy_WhenFileExists_CorrectPolicyIsRead(t *testing.T) { + got, err := maintenancewindows.InitializeMaintenanceWindow(logr.Logger{}, "testdata", "policy") + require.NoError(t, err) + + ruleOneBeginTime, err := parseTime("01:00:00+00:00") + require.NoError(t, err) + ruleOneEndTime, err := parseTime("01:00:00+00:00") + require.NoError(t, err) + + ruleTwoBeginTime, err := parseTime("21:00:00+00:00") + require.NoError(t, err) + ruleTwoEndTime, err := parseTime("00:00:00+00:00") + require.NoError(t, err) + + defaultBeginTime, err := parseTime("21:00:00+00:00") + require.NoError(t, err) + defaultEndTime, err := parseTime("23:00:00+00:00") + require.NoError(t, err) + + expectedPolicy := &resolver.MaintenanceWindowPolicy{ + Rules: []resolver.MaintenancePolicyRule{ + { + Match: resolver.MaintenancePolicyMatch{ + Plan: resolver.NewRegexp("trial|free"), + }, + Windows: resolver.MaintenanceWindows{ + { + Days: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}, + Begin: resolver.WindowTime(ruleOneBeginTime), + End: resolver.WindowTime(ruleOneEndTime), + }, + }, + }, + { + Match: resolver.MaintenancePolicyMatch{ + Region: resolver.NewRegexp("europe|eu-|uksouth"), + }, + Windows: resolver.MaintenanceWindows{ + { + Days: []string{"Sat"}, + Begin: resolver.WindowTime(ruleTwoBeginTime), + End: resolver.WindowTime(ruleTwoEndTime), + }, + }, + }, + }, + Default: resolver.MaintenanceWindow{ + Days: []string{"Sat"}, + Begin: resolver.WindowTime(defaultBeginTime), + End: resolver.WindowTime(defaultEndTime), + }, + } + + require.NoError(t, err) + require.Equal(t, expectedPolicy, got.MaintenanceWindowPolicy) +} + +func parseTime(value string) (time.Time, error) { + t, err := time.Parse("15:04:05Z07:00", value) + if err != nil { + return time.Time{}, fmt.Errorf("failed to parse time: %w", err) + } + + return t, nil +} + +var installedModuleStatus = v1beta2.ModuleStatus{ + Name: "module-name", + Version: "1.0.0", +} + +func Test_IsRequired_Returns_False_WhenNotRequiringDowntime(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder(). + WithModuleStatus(installedModuleStatus). + Build() + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("2.0.0"). + WithModuleName(installedModuleStatus.Name). + WithRequiresDowntime(false). + Build() + + result := maintenanceWindow.IsRequired(moduleTemplate, kyma) + + assert.False(t, result) +} + +func Test_IsRequired_Returns_False_WhenSkippingMaintenanceWindows(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder(). + WithModuleStatus(installedModuleStatus). + WithSkipMaintenanceWindows(true). + Build() + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("2.0.0"). + WithModuleName(installedModuleStatus.Name). + WithRequiresDowntime(true). + Build() + + result := maintenanceWindow.IsRequired(moduleTemplate, kyma) + + assert.False(t, result) +} + +func Test_IsRequired_Returns_False_WhenModuleIsNotInstalledYet(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder(). + WithSkipMaintenanceWindows(false). + Build() + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("2.0.0"). + WithModuleName(installedModuleStatus.Name). + WithRequiresDowntime(false). + Build() + + result := maintenanceWindow.IsRequired(moduleTemplate, kyma) + + assert.False(t, result) +} + +func Test_IsRequired_Returns_False_WhenSameVersionIsAlreadyInstalled(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder(). + WithModuleStatus(installedModuleStatus). + WithSkipMaintenanceWindows(false). + Build() + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("1.0.0"). + WithModuleName(installedModuleStatus.Name). + WithRequiresDowntime(true). + Build() + + result := maintenanceWindow.IsRequired(moduleTemplate, kyma) + + assert.False(t, result) +} + +func Test_IsRequired_Returns_True_WhenMaintenanceWindowIsRequire(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder(). + WithModuleStatus(installedModuleStatus). + WithSkipMaintenanceWindows(false). + Build() + moduleTemplate := builder.NewModuleTemplateBuilder(). + WithVersion("2.0.0"). + WithModuleName(installedModuleStatus.Name). + WithRequiresDowntime(true). + Build() + + result := maintenanceWindow.IsRequired(moduleTemplate, kyma) + + assert.True(t, result) +} + +func Test_IsActive_Returns_Error_WhenResolvingMaintenanceWindowPolicyFails(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowErrorStub{}, + } + + kyma := builder.NewKymaBuilder().Build() + + result, err := maintenanceWindow.IsActive(kyma) + + assert.False(t, result) + require.Error(t, err) +} + +func Test_IsActive_Returns_False_WhenOutsideMaintenanceWindow(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowInactiveStub{}, + } + + kyma := builder.NewKymaBuilder().Build() + + result, err := maintenanceWindow.IsActive(kyma) + + assert.False(t, result) + require.NoError(t, err) +} + +func Test_IsActive_Returns_True_WhenInsideMaintenanceWindow(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowActiveStub{}, + } + + kyma := builder.NewKymaBuilder().Build() + + result, err := maintenanceWindow.IsActive(kyma) + + assert.True(t, result) + require.NoError(t, err) +} + +func Test_IsActive_PassesRuntimeArgumentCorrectly(t *testing.T) { + receivedRuntime := resolver.Runtime{} + maintenanceWindowPolicyStub := maintenanceWindowRuntimeArgStub{ + receivedRuntime: &receivedRuntime, + } + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: maintenanceWindowPolicyStub, + } + + runtime := resolver.Runtime{ + GlobalAccountID: random.Name(), + Region: random.Name(), + PlatformRegion: random.Name(), + Plan: random.Name(), + } + kyma := builder.NewKymaBuilder(). + WithLabel(shared.GlobalAccountIDLabel, runtime.GlobalAccountID). + WithLabel(shared.RegionLabel, runtime.Region). + WithLabel(shared.PlatformRegionLabel, runtime.PlatformRegion). + WithLabel(shared.PlanLabel, runtime.Plan). + Build() + + result, err := maintenanceWindow.IsActive(kyma) + + assert.False(t, result) + require.NoError(t, err) + assert.Equal(t, runtime, receivedRuntime) +} + +func Test_IsActive_Returns_False_And_Error_WhenNoPolicyConfigured(t *testing.T) { + maintenanceWindow := maintenancewindows.MaintenanceWindow{ + MaintenanceWindowPolicy: nil, + } + + kyma := builder.NewKymaBuilder().Build() + + result, err := maintenanceWindow.IsActive(kyma) + + assert.False(t, result) + require.ErrorIs(t, err, maintenancewindows.ErrNoMaintenanceWindowPolicyConfigured) +} + +// test stubs + +type maintenanceWindowInactiveStub struct{} + +func (s maintenanceWindowInactiveStub) Resolve(runtime *resolver.Runtime, opts ...interface{}) (*resolver.ResolvedWindow, error) { + return &resolver.ResolvedWindow{ + Begin: time.Now().Add(1 * time.Hour), + End: time.Now().Add(2 * time.Hour), + }, nil +} + +type maintenanceWindowActiveStub struct{} + +func (s maintenanceWindowActiveStub) Resolve(runtime *resolver.Runtime, opts ...interface{}) (*resolver.ResolvedWindow, error) { + return &resolver.ResolvedWindow{ + Begin: time.Now().Add(-1 * time.Hour), + End: time.Now().Add(1 * time.Hour), + }, nil +} + +type maintenanceWindowErrorStub struct{} + +func (s maintenanceWindowErrorStub) Resolve(runtime *resolver.Runtime, opts ...interface{}) (*resolver.ResolvedWindow, error) { + return &resolver.ResolvedWindow{}, errors.New("test error") +} + +type maintenanceWindowRuntimeArgStub struct { + receivedRuntime *resolver.Runtime +} + +func (s maintenanceWindowRuntimeArgStub) Resolve(runtime *resolver.Runtime, opts ...interface{}) (*resolver.ResolvedWindow, error) { + *s.receivedRuntime = *runtime + + return &resolver.ResolvedWindow{}, nil +} diff --git a/pkg/templatelookup/regular.go b/pkg/templatelookup/regular.go index 79a759e060..f617215507 100644 --- a/pkg/templatelookup/regular.go +++ b/pkg/templatelookup/regular.go @@ -25,22 +25,33 @@ var ( ErrTemplateUpdateNotAllowed = errors.New("module template update not allowed") ) +type MaintenanceWindow interface { + IsRequired(moduleTemplate *v1beta2.ModuleTemplate, kyma *v1beta2.Kyma) bool + IsActive(kyma *v1beta2.Kyma) (bool, error) +} + type ModuleTemplateInfo struct { *v1beta2.ModuleTemplate - Err error - DesiredChannel string + Err error + WaitingForNextMaintenanceWindow bool + DesiredChannel string } -func NewTemplateLookup(reader client.Reader, descriptorProvider *provider.CachedDescriptorProvider) *TemplateLookup { +func NewTemplateLookup(reader client.Reader, + descriptorProvider *provider.CachedDescriptorProvider, + maintenanceWindow MaintenanceWindow, +) *TemplateLookup { return &TemplateLookup{ Reader: reader, descriptorProvider: descriptorProvider, + maintenanceWindow: maintenanceWindow, } } type TemplateLookup struct { client.Reader descriptorProvider *provider.CachedDescriptorProvider + maintenanceWindow MaintenanceWindow } type ModuleTemplatesByModuleName map[string]*ModuleTemplateInfo diff --git a/pkg/templatelookup/regular_test.go b/pkg/templatelookup/regular_test.go index a024b8f25d..98a9175309 100644 --- a/pkg/templatelookup/regular_test.go +++ b/pkg/templatelookup/regular_test.go @@ -332,7 +332,7 @@ func Test_GetRegularTemplates_WhenInvalidModuleProvided(t *testing.T) { for _, tt := range tests { test := tt t.Run(tt.name, func(t *testing.T) { - lookup := templatelookup.NewTemplateLookup(nil, provider.NewCachedDescriptorProvider()) + lookup := templatelookup.NewTemplateLookup(nil, provider.NewCachedDescriptorProvider(), maintenanceWindowStub{}) kyma := &v1beta2.Kyma{ Spec: test.KymaSpec, Status: test.KymaStatus, @@ -466,7 +466,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchModuleChannel(t *testing.T t.Run(testCase.name, func(t *testing.T) { lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(testCase.availableModuleTemplate, testCase.availableModuleReleaseMeta), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) for key, module := range got { @@ -539,7 +540,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchBetweenModuleVersions(t *t t.Run(testCase.name, func(t *testing.T) { lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, availableModuleReleaseMetas), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) for key, module := range got { @@ -631,7 +633,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromChannelToVersion(t *te t.Run(testCase.name, func(t *testing.T) { lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, availableModuleReleaseMetas), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) for key, module := range got { @@ -723,7 +726,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenSwitchFromVersionToChannel(t *te t.Run(testCase.name, func(t *testing.T) { lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(availableModuleTemplates, availableModuleReleaseMetas), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Len(t, got, 1) for key, module := range got { @@ -836,7 +840,8 @@ func TestNewTemplateLookup_GetRegularTemplates_WhenModuleTemplateContainsInvalid } lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, moduleReleaseMetas), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) for key, module := range got { @@ -898,7 +903,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateNotFound(t *testin givenTemplateList := &v1beta2.ModuleTemplateList{} lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, v1beta2.ModuleReleaseMetaList{}), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) for key, module := range got { @@ -1035,7 +1041,8 @@ func TestTemplateLookup_GetRegularTemplates_WhenModuleTemplateExists(t *testing. } lookup := templatelookup.NewTemplateLookup(NewFakeModuleTemplateReader(*givenTemplateList, moduleReleaseMetas), - provider.NewCachedDescriptorProvider()) + provider.NewCachedDescriptorProvider(), + maintenanceWindowStub{}) got := lookup.GetRegularTemplates(context.TODO(), testCase.kyma) assert.Equal(t, len(got), len(testCase.want)) for key, module := range got { @@ -1192,3 +1199,13 @@ func (mtlb *ModuleTemplateListBuilder) Build() v1beta2.ModuleTemplateList { func moduleToInstallByVersion(moduleName, moduleVersion string) v1beta2.Module { return testutils.NewTestModuleWithChannelVersion(moduleName, "", moduleVersion) } + +type maintenanceWindowStub struct{} + +func (m maintenanceWindowStub) IsRequired(moduleTemplate *v1beta2.ModuleTemplate, kyma *v1beta2.Kyma) bool { + return false +} + +func (m maintenanceWindowStub) IsActive(kyma *v1beta2.Kyma) (bool, error) { + return false, nil +} diff --git a/pkg/testutils/builder/kyma.go b/pkg/testutils/builder/kyma.go index 2b1cbba064..219029a1e2 100644 --- a/pkg/testutils/builder/kyma.go +++ b/pkg/testutils/builder/kyma.go @@ -122,6 +122,12 @@ func (kb KymaBuilder) WithInternal(internal bool) KymaBuilder { return kb } +// WithSkipMaintenanceWindows sets v1beta2.Kyma.Spec.SkipMaintenanceWindows. +func (kb KymaBuilder) WithSkipMaintenanceWindows(skip bool) KymaBuilder { + kb.kyma.Spec.SkipMaintenanceWindows = skip + return kb +} + // Build returns the built v1beta2.Kyma. func (kb KymaBuilder) Build() *v1beta2.Kyma { return kb.kyma diff --git a/pkg/testutils/builder/moduletemplate.go b/pkg/testutils/builder/moduletemplate.go index 2ba4bf1b5a..b9c87ac5d5 100644 --- a/pkg/testutils/builder/moduletemplate.go +++ b/pkg/testutils/builder/moduletemplate.go @@ -141,6 +141,11 @@ func (m ModuleTemplateBuilder) WithOCMPrivateRepo() ModuleTemplateBuilder { return m } +func (m ModuleTemplateBuilder) WithRequiresDowntime(value bool) ModuleTemplateBuilder { + m.moduleTemplate.Spec.RequiresDowntime = value + return m +} + func (m ModuleTemplateBuilder) Build() *v1beta2.ModuleTemplate { return m.moduleTemplate } diff --git a/pkg/testutils/moduletemplate.go b/pkg/testutils/moduletemplate.go index 395020c25c..707dcb8a07 100644 --- a/pkg/testutils/moduletemplate.go +++ b/pkg/testutils/moduletemplate.go @@ -33,7 +33,8 @@ func GetModuleTemplate(ctx context.Context, namespace string, ) (*v1beta2.ModuleTemplate, error) { descriptorProvider := provider.NewCachedDescriptorProvider() - templateLookup := templatelookup.NewTemplateLookup(clnt, descriptorProvider) + // replace maintenancePolicyHandlerStub with proper implementation for tests + templateLookup := templatelookup.NewTemplateLookup(clnt, descriptorProvider, maintenanceWindowStub{}) availableModule := templatelookup.ModuleInfo{ Module: module, } @@ -170,3 +171,13 @@ func ReadModuleVersionFromModuleTemplate(ctx context.Context, clnt client.Client return ocmDesc.Version, nil } + +type maintenanceWindowStub struct{} + +func (m maintenanceWindowStub) IsRequired(moduleTemplate *v1beta2.ModuleTemplate, kyma *v1beta2.Kyma) bool { + return false +} + +func (m maintenanceWindowStub) IsActive(kyma *v1beta2.Kyma) (bool, error) { + return false, nil +} diff --git a/tests/integration/controller/kcp/suite_test.go b/tests/integration/controller/kcp/suite_test.go index a29f9c504e..784209be82 100644 --- a/tests/integration/controller/kcp/suite_test.go +++ b/tests/integration/controller/kcp/suite_test.go @@ -42,11 +42,13 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/crd" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/event" + "github.com/kyma-project/lifecycle-manager/internal/maintenancewindows" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" "github.com/kyma-project/lifecycle-manager/pkg/log" "github.com/kyma-project/lifecycle-manager/pkg/queue" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" . "github.com/kyma-project/lifecycle-manager/pkg/testutils" "github.com/kyma-project/lifecycle-manager/tests/integration" testskrcontext "github.com/kyma-project/lifecycle-manager/tests/integration/commontestutils/skrcontextimpl" @@ -82,7 +84,8 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.TODO()) - logf.SetLogger(log.ConfigLogger(9, zapcore.AddSync(GinkgoWriter))) + logr := log.ConfigLogger(9, zapcore.AddSync(GinkgoWriter)) + logf.SetLogger(logr) var err error By("bootstrapping test environment") @@ -140,6 +143,7 @@ var _ = BeforeSuite(func() { testSkrContextFactory = testskrcontext.NewDualClusterFactory(kcpClient.Scheme(), testEventRec) descriptorProvider = provider.NewCachedDescriptorProvider() crdCache = crd.NewCache(nil) + maintenanceWindow, _ := maintenancewindows.InitializeMaintenanceWindow(logr, "/not-required", "not-required") err = (&kyma.Reconciler{ Client: kcpClient, SkrContextFactory: testSkrContextFactory, @@ -152,6 +156,7 @@ var _ = BeforeSuite(func() { IsManagedKyma: true, Metrics: metrics.NewKymaMetrics(metrics.NewSharedMetrics()), RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, flags.DefaultRemoteSyncNamespace), + TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, maintenanceWindow), }).SetupWithManager(mgr, ctrlruntime.Options{}, kyma.SetupOptions{ListenerAddr: UseRandomPort}) Expect(err).ToNot(HaveOccurred()) diff --git a/tests/integration/controller/kyma/suite_test.go b/tests/integration/controller/kyma/suite_test.go index 22f39c1b08..0e43ca2a3f 100644 --- a/tests/integration/controller/kyma/suite_test.go +++ b/tests/integration/controller/kyma/suite_test.go @@ -41,11 +41,13 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/controller/kyma" "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" "github.com/kyma-project/lifecycle-manager/internal/event" + "github.com/kyma-project/lifecycle-manager/internal/maintenancewindows" "github.com/kyma-project/lifecycle-manager/internal/pkg/flags" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" "github.com/kyma-project/lifecycle-manager/pkg/log" "github.com/kyma-project/lifecycle-manager/pkg/queue" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" "github.com/kyma-project/lifecycle-manager/tests/integration" testskrcontext "github.com/kyma-project/lifecycle-manager/tests/integration/commontestutils/skrcontextimpl" @@ -80,7 +82,8 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { ctx, cancel = context.WithCancel(context.TODO()) - logf.SetLogger(log.ConfigLogger(9, zapcore.AddSync(GinkgoWriter))) + logr := log.ConfigLogger(9, zapcore.AddSync(GinkgoWriter)) + logf.SetLogger(logr) By("bootstrapping test environment") @@ -134,6 +137,7 @@ var _ = BeforeSuite(func() { kcpClient = mgr.GetClient() testEventRec := event.NewRecorderWrapper(mgr.GetEventRecorderFor(shared.OperatorName)) testSkrContextFactory := testskrcontext.NewSingleClusterFactory(kcpClient, mgr.GetConfig(), testEventRec) + maintenanceWindow, _ := maintenancewindows.InitializeMaintenanceWindow(logr, "/not-required", "/not-required") err = (&kyma.Reconciler{ Client: kcpClient, Event: testEventRec, @@ -144,6 +148,7 @@ var _ = BeforeSuite(func() { InKCPMode: false, RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, Metrics: metrics.NewKymaMetrics(metrics.NewSharedMetrics()), + TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, maintenanceWindow), }).SetupWithManager(mgr, ctrlruntime.Options{ RateLimiter: internal.RateLimiter( 1*time.Second, 5*time.Second,