From 2dfa5de0573ac6af3f7ade13ce20d6a42b1da8e9 Mon Sep 17 00:00:00 2001 From: Amritanshu Sikdar Date: Mon, 20 Jan 2025 16:46:49 +0100 Subject: [PATCH 1/2] docs: Reference Mandatory Modules Controller in Architecture Docs (#2197) * reference controller in architecture docs * fix markdown lint * improve docs --- docs/contributor/01-architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributor/01-architecture.md b/docs/contributor/01-architecture.md index 2e9b37743e..5f05104bc1 100644 --- a/docs/contributor/01-architecture.md +++ b/docs/contributor/01-architecture.md @@ -32,6 +32,7 @@ Apart from the custom resources, Lifecycle Manager uses also Kyma, Manifest, and * [Kyma controller](../../internal/controller/kyma/controller.go) - reconciles the Kyma CR which means creating Manifest CRs for each Kyma module enabled in the Kyma CR and deleting them when modules are disabled in the Kyma CR. It is also responsible for synchronising ModuleTemplate CRs between KCP and Kyma runtimes. * [Manifest controller](../../internal/controller/manifest/controller.go) - reconciles the Manifest CRs created by the Kyma controller, which means, installing components specified in the Manifest CR in the target SKR cluster and removing them when the Manifest CRs are flagged for deletion. +* [Mandatory Modules controller](02-controllers.md#mandatory-modules-controllers) - reconciles the mandatory ModuleTemplate CRs that have the `operator.kyma-project.io/mandatory-module` label, selecting the highest version if duplicates exist. It translates the ModuleTemplate CRs to Manifest CRs linked to the Kyma CR, ensuring changes propagate. For removal, a deletion controller marks the related Manifest CRs, removes finalizers, and deletes the ModuleTemplate CR. * [Purge controller](../../internal/controller/purge/controller.go) - reconciles the Kyma CRs that are marked for deletion longer than the grace period, which means purging all the resources deployed by Lifecycle Manager in the target SKR cluster. * [Watcher controller](../../internal/controller/watcher/controller.go) - reconciles the Watcher CR which means creating Istio Virtual Service resources in KCP when a Watcher CR is created and removing the same resources when the Watcher CR is deleted. This is done to configure the routing of the messages that come from the watcher agent, installed on each Kyma runtime, and go to a listener agent deployed in KCP. From 90b9197ea874bc23e3a6ad9574c863f30e203342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Schw=C3=A4gerl?= Date: Mon, 20 Jan 2025 17:16:01 +0100 Subject: [PATCH 2/2] feat: Implement MaintenanceWindow determination logic (#2196) * feat: Add metadata and status helpers to Kyma type * add go sum * cleanup go mod, add api to coverage * remove api from unit-test coverage * feat: MaintenanceWindow service * chore(dependabot): bump k8s.io/apimachinery from 0.32.0 to 0.32.1 in /api (#2185) chore(dependabot): bump k8s.io/apimachinery in /api Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.32.0 to 0.32.1. - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.0...v0.32.1) --- updated-dependencies: - dependency-name: k8s.io/apimachinery dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(dependabot): bump sigs.k8s.io/controller-runtime from 0.19.4 to 0.20.0 (#2192) chore(dependabot): bump sigs.k8s.io/controller-runtime Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.19.4 to 0.20.0. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.19.4...v0.20.0) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: Add metadata and status helpers to Kyma type * cleanup go mod, add api to coverage * remove obsolete comment * fix fake arguments in suite_test * avoid handler name in suite_test * rename to maintenanceWindow consistently * underscore unused receiver arg * omit receiver arg --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cmd/main.go | 14 +- internal/controller/kyma/controller.go | 3 +- .../maintenance_policy_handler.go | 44 --- .../maintenance_policy_handler_test.go | 112 ------ .../maintenancewindows/maintenance_window.go | 110 ++++++ .../maintenance_window_test.go | 338 ++++++++++++++++++ pkg/templatelookup/regular.go | 17 +- pkg/templatelookup/regular_test.go | 33 +- pkg/testutils/builder/kyma.go | 6 + pkg/testutils/builder/moduletemplate.go | 5 + pkg/testutils/moduletemplate.go | 13 +- .../integration/controller/kcp/suite_test.go | 7 +- .../integration/controller/kyma/suite_test.go | 7 +- 13 files changed, 533 insertions(+), 176 deletions(-) delete mode 100644 internal/maintenancewindows/maintenance_policy_handler.go delete mode 100644 internal/maintenancewindows/maintenance_policy_handler_test.go create mode 100644 internal/maintenancewindows/maintenance_window.go create mode 100644 internal/maintenancewindows/maintenance_window_test.go 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,