diff --git a/cmd/main.go b/cmd/main.go index 30b02f6317..bf64fc3532 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -399,7 +399,8 @@ func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDe moduleStatusGen := generator.NewModuleStatusGenerator(fromerror.GenerateModuleStatusFromError) modulesStatusHandler := modules.NewStatusHandler(moduleStatusGen, kcpClient, kymaMetrics.RemoveModuleStateMetrics) - if err := (&kyma.Reconciler{ + // Setup InstallationReconciler for handling Kyma installation and updates + if err := (&kyma.InstallationReconciler{ Client: kcpClient, SkrContextFactory: skrContextFactory, Event: event, @@ -427,7 +428,34 @@ func setupKymaReconciler(mgr ctrl.Manager, descriptorProvider *provider.CachedDe IstioNamespace: flagVar.IstioNamespace, }, ); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Kyma") + setupLog.Error(err, "unable to create controller", "controller", "KymaInstallation") + os.Exit(1) + } + + // Setup DeletionReconciler for handling Kyma deletion + if err := (&kyma.DeletionReconciler{ + Client: kcpClient, + SkrContextFactory: skrContextFactory, + Event: event, + DescriptorProvider: descriptorProvider, + SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, skrContextFactory, nil), + ModulesStatusHandler: modulesStatusHandler, + SKRWebhookManager: skrWebhookManager, + RequeueIntervals: queue.RequeueIntervals{ + Success: flagVar.KymaRequeueSuccessInterval, + Busy: flagVar.KymaRequeueBusyInterval, + Error: flagVar.KymaRequeueErrInterval, + Warning: flagVar.KymaRequeueWarningInterval, + }, + RemoteSyncNamespace: flagVar.RemoteSyncNamespace, + IsManagedKyma: flagVar.IsKymaManaged, + Metrics: kymaMetrics, + RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, skrContextFactory, + flagVar.RemoteSyncNamespace), + TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, + moduleTemplateInfoLookupStrategies), + }).SetupWithManager(mgr, options); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "KymaDeletion") os.Exit(1) } } diff --git a/internal/controller/kyma/deletion_controller.go b/internal/controller/kyma/deletion_controller.go new file mode 100644 index 0000000000..2dba9217d8 --- /dev/null +++ b/internal/controller/kyma/deletion_controller.go @@ -0,0 +1,345 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kyma + +import ( + "context" + "errors" + "fmt" + + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8slabels "k8s.io/apimachinery/pkg/labels" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/descriptor/provider" + "github.com/kyma-project/lifecycle-manager/internal/event" + "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" + "github.com/kyma-project/lifecycle-manager/internal/remote" + "github.com/kyma-project/lifecycle-manager/internal/service/accessmanager" + "github.com/kyma-project/lifecycle-manager/pkg/log" + "github.com/kyma-project/lifecycle-manager/pkg/queue" + "github.com/kyma-project/lifecycle-manager/pkg/status" + "github.com/kyma-project/lifecycle-manager/pkg/templatelookup" + "github.com/kyma-project/lifecycle-manager/pkg/util" +) + +type DeletionReconciler struct { + client.Client + event.Event + queue.RequeueIntervals + + SkrContextFactory remote.SkrContextProvider + DescriptorProvider *provider.CachedDescriptorProvider + SyncRemoteCrds remote.SyncCrdsUseCase + ModulesStatusHandler ModuleStatusHandler + SKRWebhookManager SKRWebhookManager + RemoteSyncNamespace string + IsManagedKyma bool + Metrics *metrics.KymaMetrics + RemoteCatalog *remote.RemoteCatalog + TemplateLookup *templatelookup.TemplateLookup +} + +func (r *DeletionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logf.FromContext(ctx) + + kyma := &v1beta2.Kyma{} + if err := r.Get(ctx, req.NamespacedName, kyma); err != nil { + if util.IsNotFound(err) { + logger.V(log.DebugLevel).Info(fmt.Sprintf("Kyma %s not found, probably already deleted", + req.NamespacedName)) + if err = r.deleteOrphanedCertificate(ctx, req.Name); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + r.Metrics.RecordRequeueReason(metrics.KymaRetrieval, queue.UnexpectedRequeue) + return ctrl.Result{}, fmt.Errorf("kyma DeletionReconciler: %w", err) + } + + // Only handle deletion cases - if no deletion timestamp, let the installation controller handle it + if kyma.DeletionTimestamp.IsZero() { + logger.V(log.DebugLevel).Info("Kyma not under deletion, skipping deletion controller") + return ctrl.Result{}, nil + } + + logger.V(log.DebugLevel).Info("Kyma deletion reconciliation started") + + status.InitConditions(kyma, r.WatcherEnabled()) // check what conditions are used in the deletion use-case + + if kyma.SkipReconciliation() { + logger.V(log.DebugLevel).Info("skipping deletion reconciliation for Kyma: " + kyma.Name) + return ctrl.Result{RequeueAfter: r.Success}, nil + } + + err := r.SkrContextFactory.Init(ctx, kyma.GetNamespacedName()) + if errors.Is(err, accessmanager.ErrAccessSecretNotFound) { + return r.handleDeletedSkr(ctx, kyma) + } + + skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) + if err != nil { + r.Metrics.RecordRequeueReason(metrics.SyncContextRetrieval, queue.UnexpectedRequeue) + setModuleStatusesToError(kyma, err.Error()) + return r.requeueWithError(ctx, kyma, err) + } + + return r.reconcileDeletion(ctx, kyma, skrContext) +} + +func (r *DeletionReconciler) reconcileDeletion(ctx context.Context, + kyma *v1beta2.Kyma, + _ *remote.SkrContext, +) (ctrl.Result, error) { + if kyma.Status.State != shared.StateDeleting { + if err := r.deleteRemoteKyma(ctx, kyma); err != nil { + r.Metrics.RecordRequeueReason(metrics.RemoteKymaDeletion, queue.UnexpectedRequeue) + return r.requeueWithError(ctx, kyma, err) + } + if err := r.updateStatus(ctx, kyma, shared.StateDeleting, "waiting for modules to be deleted"); err != nil { + r.Metrics.RecordRequeueReason(metrics.StatusUpdateToDeleting, queue.UnexpectedRequeue) + return r.requeueWithError(ctx, kyma, + fmt.Errorf("could not update kyma status after triggering deletion: %w", err)) + } + r.Metrics.RecordRequeueReason(metrics.StatusUpdateToDeleting, queue.IntendedRequeue) + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil + } + + return r.handleDeletingState(ctx, kyma) +} + +func (r *DeletionReconciler) handleDeletedSkr(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { + logf.FromContext(ctx).Info("access secret not found for kyma, assuming already deleted cluster") + if err := r.cleanupManifestCRs(ctx, kyma); err != nil { + r.Metrics.RecordRequeueReason(metrics.CleanupManifestCrs, queue.UnexpectedRequeue) + return ctrl.Result{}, err + } + r.cleanupMetrics(kyma.Name) + r.removeAllFinalizers(kyma) + + if err := r.updateKyma(ctx, kyma); err != nil { + r.Metrics.RecordRequeueReason(metrics.KymaUnderDeletionAndAccessSecretNotFound, queue.UnexpectedRequeue) + return ctrl.Result{}, err + } + r.Metrics.RecordRequeueReason(metrics.KymaUnderDeletionAndAccessSecretNotFound, queue.IntendedRequeue) + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil +} + +func (r *DeletionReconciler) deleteRemoteKyma(ctx context.Context, kyma *v1beta2.Kyma) error { + skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) + if err != nil { + return fmt.Errorf("failed to get skrContext: %w", err) + } + if err := skrContext.DeleteKyma(ctx); client.IgnoreNotFound(err) != nil { + logf.FromContext(ctx).V(log.InfoLevel).Error(err, "Failed to be deleted remotely!") + return fmt.Errorf("error occurred while trying to delete remotely synced kyma: %w", err) + } + logf.FromContext(ctx).V(log.InfoLevel).Info("Successfully deleted remotely!") + + return nil +} + +func (r *DeletionReconciler) handleDeletingState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { + logger := logf.FromContext(ctx).V(log.InfoLevel) + + if r.WatcherEnabled() { + if err := r.SKRWebhookManager.Remove(ctx, kyma); err != nil { + return ctrl.Result{}, err + } + } + + if err := r.RemoteCatalog.Delete(ctx, kyma.GetNamespacedName()); err != nil { + err = fmt.Errorf("failed to delete remote module catalog: %w", err) + r.Metrics.RecordRequeueReason(metrics.RemoteModuleCatalogDeletion, queue.UnexpectedRequeue) + return r.requeueWithError(ctx, kyma, err) + } + skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get skrContext: %w", err) + } + + r.SkrContextFactory.InvalidateCache(kyma.GetNamespacedName()) + if err = skrContext.RemoveFinalizersFromKyma(ctx); client.IgnoreNotFound(err) != nil { + r.Metrics.RecordRequeueReason(metrics.FinalizersRemovalFromRemoteKyma, queue.UnexpectedRequeue) + return r.requeueWithError(ctx, kyma, err) + } + + logger.Info("removed remote finalizers") + + if err := r.cleanupManifestCRs(ctx, kyma); err != nil { + r.Metrics.RecordRequeueReason(metrics.CleanupManifestCrs, queue.UnexpectedRequeue) + return ctrl.Result{}, err + } + + r.cleanupMetrics(kyma.Name) + controllerutil.RemoveFinalizer(kyma, shared.KymaFinalizer) + + if err := r.updateKyma(ctx, kyma); err != nil { + r.Metrics.RecordRequeueReason(metrics.KymaDeletion, queue.UnexpectedRequeue) + return ctrl.Result{}, err + } + r.Metrics.RecordRequeueReason(metrics.KymaDeletion, queue.IntendedRequeue) + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil +} + +// Methods required by HelperClient interface +func (r *DeletionReconciler) WatcherEnabled() bool { + return r.SKRWebhookManager != nil +} + +func (r *DeletionReconciler) IsKymaManaged() bool { + return r.IsManagedKyma +} + +func (r *DeletionReconciler) UpdateMetrics(ctx context.Context, kyma *v1beta2.Kyma) { + if err := r.Metrics.UpdateAll(kyma); err != nil { + if metrics.IsMissingMetricsAnnotationOrLabel(err) { + r.Event.Warning(kyma, metricsError, err) + } + logf.FromContext(ctx).V(log.DebugLevel).Info(fmt.Sprintf("error occurred while updating all metrics: %s", err)) + } +} + +// Helper methods for deletion controller +func (r *DeletionReconciler) cleanupMetrics(kymaName string) { + r.Metrics.CleanupMetrics(kymaName) +} + +func (r *DeletionReconciler) cleanupManifestCRs(ctx context.Context, kyma *v1beta2.Kyma) error { + relatedManifests, err := r.getRelatedManifestCRs(ctx, kyma) + if err != nil { + return fmt.Errorf("error while trying to get manifests: %w", err) + } + + if r.relatedManifestCRsAreDeleted(relatedManifests) { + return nil + } + + if err = r.deleteManifests(ctx, relatedManifests); err != nil { + return fmt.Errorf("error while trying to delete manifests: %w", err) + } + return ErrManifestsStillExist +} + +func (r *DeletionReconciler) deleteManifests(ctx context.Context, manifests []v1beta2.Manifest) error { + for i := range manifests { + if err := r.Delete(ctx, &manifests[i]); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("error while trying to delete manifest: %w", err) + } + } + return nil +} + +func (r *DeletionReconciler) getRelatedManifestCRs(ctx context.Context, + kyma *v1beta2.Kyma, +) ([]v1beta2.Manifest, error) { + manifestList := &v1beta2.ManifestList{} + labelSelector := k8slabels.SelectorFromSet(k8slabels.Set{shared.KymaName: kyma.Name}) + if err := r.List(ctx, manifestList, + &client.ListOptions{LabelSelector: labelSelector}); client.IgnoreNotFound(err) != nil { + return nil, fmt.Errorf("failed to get related manifests, %w", err) + } + + return manifestList.Items, nil +} + +func (r *DeletionReconciler) relatedManifestCRsAreDeleted(manifests []v1beta2.Manifest) bool { + return len(manifests) == 0 +} + +func (r *DeletionReconciler) removeAllFinalizers(kyma *v1beta2.Kyma) { + for _, finalizer := range kyma.Finalizers { + controllerutil.RemoveFinalizer(kyma, finalizer) + } +} + +func (r *DeletionReconciler) updateKyma(ctx context.Context, kyma *v1beta2.Kyma) error { + if err := r.Update(ctx, kyma); err != nil { + err = fmt.Errorf("error while updating kyma during deletion: %w", err) + r.Event.Warning(kyma, updateSpecError, err) + return err + } + + return nil +} + +func (r *DeletionReconciler) updateStatus(ctx context.Context, kyma *v1beta2.Kyma, + state shared.State, message string, +) error { + if err := status.Helper(r).UpdateStatusForExistingModules(ctx, kyma, state, message); err != nil { + r.Event.Warning(kyma, patchStatusError, err) + return fmt.Errorf("error while updating status to %s because of %s: %w", state, message, err) + } + return nil +} + +func (r *DeletionReconciler) updateStatusWithError(ctx context.Context, kyma *v1beta2.Kyma, err error) error { + if err := status.Helper(r).UpdateStatusForExistingModules(ctx, kyma, shared.StateError, err.Error()); err != nil { + r.Event.Warning(kyma, updateStatusError, err) + return fmt.Errorf("error while updating status to %s: %w", shared.StateError, err) + } + return nil +} + +func (r *DeletionReconciler) requeueWithError(ctx context.Context, kyma *v1beta2.Kyma, err error) (ctrl.Result, error) { + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, r.updateStatusWithError(ctx, kyma, err) +} + +func (r *DeletionReconciler) deleteManifest(ctx context.Context, trackedManifest *v1beta2.TrackingObject) error { + manifest := apimetav1.PartialObjectMetadata{} + manifest.SetGroupVersionKind(trackedManifest.GroupVersionKind()) + manifest.SetNamespace(trackedManifest.GetNamespace()) + manifest.SetName(trackedManifest.GetName()) + + err := r.Delete(ctx, &manifest, &client.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed delete manifest crd: %w", err) + } + return nil +} + +func (r *DeletionReconciler) deleteOrphanedCertificate(ctx context.Context, kymaName string) error { + if !r.WatcherEnabled() { + return nil + } + return r.SKRWebhookManager.RemoveSkrCertificate(ctx, kymaName) +} + +func (r *DeletionReconciler) DeleteNoLongerExistingModules(ctx context.Context, kyma *v1beta2.Kyma) error { + moduleStatus := kyma.GetNoLongerExistingModuleStatus() + var err error + if len(moduleStatus) == 0 { + return nil + } + for i := range moduleStatus { + moduleStatus := moduleStatus[i] + if moduleStatus.Manifest == nil { + continue + } + err = r.deleteManifest(ctx, moduleStatus.Manifest) + } + + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("error deleting module %w", err) + } + return nil +} diff --git a/internal/controller/kyma/controller.go b/internal/controller/kyma/installation_controller.go similarity index 55% rename from internal/controller/kyma/controller.go rename to internal/controller/kyma/installation_controller.go index 243f4aa4e7..2f53b62f82 100644 --- a/internal/controller/kyma/controller.go +++ b/internal/controller/kyma/installation_controller.go @@ -24,10 +24,8 @@ import ( "golang.org/x/sync/errgroup" apierrors "k8s.io/apimachinery/pkg/api/errors" apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8slabels "k8s.io/apimachinery/pkg/labels" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" "github.com/kyma-project/lifecycle-manager/api/shared" @@ -37,9 +35,7 @@ import ( "github.com/kyma-project/lifecycle-manager/internal/manifest/parser" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/remote" - "github.com/kyma-project/lifecycle-manager/internal/service/accessmanager" "github.com/kyma-project/lifecycle-manager/pkg/log" - modulecommon "github.com/kyma-project/lifecycle-manager/pkg/module/common" "github.com/kyma-project/lifecycle-manager/pkg/module/sync" "github.com/kyma-project/lifecycle-manager/pkg/queue" "github.com/kyma-project/lifecycle-manager/pkg/status" @@ -48,30 +44,7 @@ import ( "github.com/kyma-project/lifecycle-manager/pkg/watcher" ) -var ( - ErrManifestsStillExist = errors.New("manifests still exist") - ErrInvalidKymaSpec = errors.New("invalid kyma spec") - ErrKymaInErrorState = errors.New("kyma in error state") -) - -const ( - metricsError event.Reason = "MetricsError" - updateSpecError event.Reason = "UpdateSpecError" - updateStatusError event.Reason = "UpdateStatusError" - patchStatusError event.Reason = "PatchStatus" -) - -type SKRWebhookManager interface { - Reconcile(ctx context.Context, kyma *v1beta2.Kyma) error - Remove(ctx context.Context, kyma *v1beta2.Kyma) error - RemoveSkrCertificate(ctx context.Context, kymaName string) error -} - -type ModuleStatusHandler interface { - UpdateModuleStatuses(ctx context.Context, kyma *v1beta2.Kyma, modules modulecommon.Modules) error -} - -type Reconciler struct { +type InstallationReconciler struct { client.Client event.Event queue.RequeueIntervals @@ -88,26 +61,29 @@ type Reconciler struct { TemplateLookup *templatelookup.TemplateLookup } -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := logf.FromContext(ctx) - logger.V(log.DebugLevel).Info("Kyma reconciliation started") - kyma := &v1beta2.Kyma{} if err := r.Get(ctx, req.NamespacedName, kyma); err != nil { if util.IsNotFound(err) { logger.V(log.DebugLevel).Info(fmt.Sprintf("Kyma %s not found, probably already deleted", req.NamespacedName)) - if err = r.deleteOrphanedCertificate(ctx, req.Name); err != nil { - return ctrl.Result{}, err - } return ctrl.Result{}, nil } r.Metrics.RecordRequeueReason(metrics.KymaRetrieval, queue.UnexpectedRequeue) - return ctrl.Result{}, fmt.Errorf("KymaController: %w", err) + return ctrl.Result{}, fmt.Errorf("kyma DeletionReconciler: %w", err) } + // If Kyma is under deletion, let the deletion controller handle it + if !kyma.DeletionTimestamp.IsZero() { + logger.V(log.DebugLevel).Info("Kyma under deletion, leaving to deletion controller") + return ctrl.Result{}, nil + } + + logger.V(log.DebugLevel).Info("Kyma installation reconciliation started") + if err := r.UpdateModuleTemplatesIfNeeded(ctx); err != nil { - return ctrl.Result{}, fmt.Errorf("KymaController: %w", err) + return ctrl.Result{}, fmt.Errorf("KymaInstallationController: %w", err) } status.InitConditions(kyma, r.WatcherEnabled()) @@ -118,10 +94,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu } err := r.SkrContextFactory.Init(ctx, kyma.GetNamespacedName()) - if !kyma.DeletionTimestamp.IsZero() && errors.Is(err, accessmanager.ErrAccessSecretNotFound) { - return r.handleDeletedSkr(ctx, kyma) + if err != nil { + return ctrl.Result{}, fmt.Errorf("KymaInstallationController: %w", err) } - skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) if err != nil { r.Metrics.RecordRequeueReason(metrics.SyncContextRetrieval, queue.UnexpectedRequeue) @@ -143,123 +118,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return r.requeueWithError(ctx, kyma, err) } - return r.reconcile(ctx, kyma) -} - -// ValidateDefaultChannel validates the Kyma spec. -func (r *Reconciler) ValidateDefaultChannel(kyma *v1beta2.Kyma) error { - if shared.NoneChannel.Equals(kyma.Spec.Channel) { - return fmt.Errorf("%w: value \"none\" is not allowed in spec.channel", ErrInvalidKymaSpec) - } - return nil -} - -func (r *Reconciler) DeleteNoLongerExistingModules(ctx context.Context, kyma *v1beta2.Kyma) error { - moduleStatus := kyma.GetNoLongerExistingModuleStatus() - var err error - if len(moduleStatus) == 0 { - return nil - } - for i := range moduleStatus { - moduleStatus := moduleStatus[i] - if moduleStatus.Manifest == nil { - continue - } - err = r.deleteManifest(ctx, moduleStatus.Manifest) - } - - if client.IgnoreNotFound(err) != nil { - return fmt.Errorf("error deleting module %w", err) - } - return nil -} - -func (r *Reconciler) UpdateMetrics(ctx context.Context, kyma *v1beta2.Kyma) { - if err := r.Metrics.UpdateAll(kyma); err != nil { - if metrics.IsMissingMetricsAnnotationOrLabel(err) { - r.Event.Warning(kyma, metricsError, err) - } - logf.FromContext(ctx).V(log.DebugLevel).Info(fmt.Sprintf("error occurred while updating all metrics: %s", err)) - } -} - -func (r *Reconciler) WatcherEnabled() bool { - return r.SKRWebhookManager != nil -} - -func (r *Reconciler) IsKymaManaged() bool { - return r.IsManagedKyma -} - -func (r *Reconciler) GetModuleTemplateList(ctx context.Context) (*v1beta2.ModuleTemplateList, error) { - moduleTemplateList := &v1beta2.ModuleTemplateList{} - if err := r.List(ctx, moduleTemplateList, &client.ListOptions{}); err != nil { - return nil, fmt.Errorf("could not aggregate module templates for module catalog sync: %w", err) - } - - return moduleTemplateList, nil -} - -func (r *Reconciler) UpdateModuleTemplatesIfNeeded(ctx context.Context) error { - moduleTemplateList, err := r.GetModuleTemplateList(ctx) - if err != nil { - return err - } - - for _, mt := range moduleTemplateList.Items { - if needUpdateForMandatoryModuleLabel(mt) { - if err = r.Update(ctx, &mt); err != nil { - return fmt.Errorf("failed to update ModuleTemplate, %w", err) - } - } - } - - return nil -} - -func (r *Reconciler) requeueWithError(ctx context.Context, kyma *v1beta2.Kyma, err error) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, r.updateStatusWithError(ctx, kyma, err) -} - -func (r *Reconciler) handleDeletedSkr(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { - logf.FromContext(ctx).Info("access secret not found for kyma, assuming already deleted cluster") - if err := r.cleanupManifestCRs(ctx, kyma); err != nil { - r.Metrics.RecordRequeueReason(metrics.CleanupManifestCrs, queue.UnexpectedRequeue) - return ctrl.Result{}, err - } - r.cleanupMetrics(kyma.Name) - r.removeAllFinalizers(kyma) - - if err := r.updateKyma(ctx, kyma); err != nil { - r.Metrics.RecordRequeueReason(metrics.KymaUnderDeletionAndAccessSecretNotFound, queue.UnexpectedRequeue) - return ctrl.Result{}, err - } - r.Metrics.RecordRequeueReason(metrics.KymaUnderDeletionAndAccessSecretNotFound, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil + return r.reconcileInstallation(ctx, kyma) } -func (r *Reconciler) reconcile(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { - if !kyma.DeletionTimestamp.IsZero() && kyma.Status.State != shared.StateDeleting { - if err := r.deleteRemoteKyma(ctx, kyma); err != nil { - r.Metrics.RecordRequeueReason(metrics.RemoteKymaDeletion, queue.UnexpectedRequeue) - return r.requeueWithError(ctx, kyma, err) - } - if err := r.updateStatus(ctx, kyma, shared.StateDeleting, "waiting for modules to be deleted"); err != nil { - r.Metrics.RecordRequeueReason(metrics.StatusUpdateToDeleting, queue.UnexpectedRequeue) - return r.requeueWithError(ctx, kyma, - fmt.Errorf("could not update kyma status after triggering deletion: %w", err)) - } - r.Metrics.RecordRequeueReason(metrics.StatusUpdateToDeleting, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil - } - +func (r *InstallationReconciler) reconcileInstallation(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { if needsUpdate := kyma.EnsureLabelsAndFinalizers(); needsUpdate { if err := r.Update(ctx, kyma); err != nil { r.Metrics.RecordRequeueReason(metrics.LabelsAndFinalizersUpdate, queue.UnexpectedRequeue) return r.requeueWithError(ctx, kyma, fmt.Errorf("failed to update kyma after finalizer check: %w", err)) } r.Metrics.RecordRequeueReason(metrics.LabelsAndFinalizersUpdate, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil } updateRequired, err := r.SyncRemoteCrds.Execute(ctx, kyma) @@ -273,7 +142,7 @@ func (r *Reconciler) reconcile(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Re return r.requeueWithError(ctx, kyma, fmt.Errorf("could not update kyma annotations: %w", err)) } r.Metrics.RecordRequeueReason(metrics.CrdAnnotationsUpdate, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil } // update the control-plane kyma with the changes to the spec of the remote Kyma if err = r.replaceSpecFromRemote(ctx, kyma); err != nil { @@ -296,21 +165,7 @@ func (r *Reconciler) reconcile(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Re return res, err } -func (r *Reconciler) deleteRemoteKyma(ctx context.Context, kyma *v1beta2.Kyma) error { - skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) - if err != nil { - return fmt.Errorf("failed to get skrContext: %w", err) - } - if err := skrContext.DeleteKyma(ctx); client.IgnoreNotFound(err) != nil { - logf.FromContext(ctx).V(log.InfoLevel).Error(err, "Failed to be deleted remotely!") - return fmt.Errorf("error occurred while trying to delete remotely synced kyma: %w", err) - } - logf.FromContext(ctx).V(log.InfoLevel).Info("Successfully deleted remotely!") - - return nil -} - -func (r *Reconciler) fetchRemoteKyma(ctx context.Context, kcpKyma *v1beta2.Kyma) (*v1beta2.Kyma, error) { +func (r *InstallationReconciler) fetchRemoteKyma(ctx context.Context, kcpKyma *v1beta2.Kyma) (*v1beta2.Kyma, error) { syncContext, err := r.SkrContextFactory.Get(kcpKyma.GetNamespacedName()) if err != nil { return nil, fmt.Errorf("failed to get syncContext: %w", err) @@ -326,7 +181,7 @@ func (r *Reconciler) fetchRemoteKyma(ctx context.Context, kcpKyma *v1beta2.Kyma) } // syncStatusToRemote updates the status of a remote copy of given Kyma instance. -func (r *Reconciler) syncStatusToRemote(ctx context.Context, kcpKyma *v1beta2.Kyma) error { +func (r *InstallationReconciler) syncStatusToRemote(ctx context.Context, kcpKyma *v1beta2.Kyma) error { remoteKyma, err := r.fetchRemoteKyma(ctx, kcpKyma) if err != nil { if errors.Is(err, remote.ErrNotFoundAndKCPKymaUnderDeleting) { @@ -353,7 +208,7 @@ func (r *Reconciler) syncStatusToRemote(ctx context.Context, kcpKyma *v1beta2.Ky } // replaceSpecFromRemote replaces the spec from control-lane Kyma with the remote Kyma spec as single source of truth. -func (r *Reconciler) replaceSpecFromRemote(ctx context.Context, controlPlaneKyma *v1beta2.Kyma) error { +func (r *InstallationReconciler) replaceSpecFromRemote(ctx context.Context, controlPlaneKyma *v1beta2.Kyma) error { remoteKyma, err := r.fetchRemoteKyma(ctx, controlPlaneKyma) if err != nil { if errors.Is(err, remote.ErrNotFoundAndKCPKymaUnderDeleting) { @@ -372,14 +227,12 @@ func (r *Reconciler) replaceSpecFromRemote(ctx context.Context, controlPlaneKyma return nil } -func (r *Reconciler) processKymaState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { +func (r *InstallationReconciler) processKymaState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { switch kyma.Status.State { case "": return r.handleInitialState(ctx, kyma) case shared.StateProcessing: return r.handleProcessingState(ctx, kyma) - case shared.StateDeleting: - return r.handleDeletingState(ctx, kyma) case shared.StateError: return r.handleProcessingState(ctx, kyma) case shared.StateReady, shared.StateWarning: @@ -391,16 +244,16 @@ func (r *Reconciler) processKymaState(ctx context.Context, kyma *v1beta2.Kyma) ( return ctrl.Result{}, nil } -func (r *Reconciler) handleInitialState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { +func (r *InstallationReconciler) handleInitialState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { if err := r.updateStatus(ctx, kyma, shared.StateProcessing, "started processing"); err != nil { r.Metrics.RecordRequeueReason(metrics.InitialStateHandling, queue.UnexpectedRequeue) return ctrl.Result{}, err } r.Metrics.RecordRequeueReason(metrics.InitialStateHandling, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, nil } -func (r *Reconciler) handleProcessingState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { +func (r *InstallationReconciler) handleProcessingState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { logger := logf.FromContext(ctx) var errGroup errgroup.Group errGroup.Go(func() error { @@ -466,145 +319,107 @@ func (r *Reconciler) handleProcessingState(ctx context.Context, kyma *v1beta2.Ky return ctrl.Result{RequeueAfter: requeueInterval}, nil } -func checkSKRWebhookReadiness(ctx context.Context, skrClient *remote.SkrContext, kyma *v1beta2.Kyma) error { - err := watcher.AssertDeploymentReady(ctx, skrClient) - if err != nil { - kyma.UpdateCondition(v1beta2.ConditionTypeSKRWebhook, apimetav1.ConditionFalse) - if errors.Is(err, watcher.ErrSkrWebhookDeploymentInBackoff) { - return err - } - return nil - } - kyma.UpdateCondition(v1beta2.ConditionTypeSKRWebhook, apimetav1.ConditionTrue) - return nil -} - -func (r *Reconciler) handleDeletingState(ctx context.Context, kyma *v1beta2.Kyma) (ctrl.Result, error) { - logger := logf.FromContext(ctx).V(log.InfoLevel) +func (r *InstallationReconciler) reconcileManifests(ctx context.Context, kyma *v1beta2.Kyma) error { + templates := r.TemplateLookup.GetRegularTemplates(ctx, kyma) + prsr := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace) + modules := prsr.GenerateModulesFromTemplates(kyma, templates) - if r.WatcherEnabled() { - if err := r.SKRWebhookManager.Remove(ctx, kyma); err != nil { - return ctrl.Result{}, err - } + runner := sync.New(r) + if err := runner.ReconcileManifests(ctx, kyma, modules); err != nil { + return fmt.Errorf("sync failed: %w", err) } - if err := r.RemoteCatalog.Delete(ctx, kyma.GetNamespacedName()); err != nil { - err = fmt.Errorf("failed to delete remote module catalog: %w", err) - r.Metrics.RecordRequeueReason(metrics.RemoteModuleCatalogDeletion, queue.UnexpectedRequeue) - return r.requeueWithError(ctx, kyma, err) - } - skrContext, err := r.SkrContextFactory.Get(kyma.GetNamespacedName()) + err := r.ModulesStatusHandler.UpdateModuleStatuses(ctx, kyma, modules) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get skrContext: %w", err) - } - - r.SkrContextFactory.InvalidateCache(kyma.GetNamespacedName()) - if err = skrContext.RemoveFinalizersFromKyma(ctx); client.IgnoreNotFound(err) != nil { - r.Metrics.RecordRequeueReason(metrics.FinalizersRemovalFromRemoteKyma, queue.UnexpectedRequeue) - return r.requeueWithError(ctx, kyma, err) - } - - logger.Info("removed remote finalizers") - - if err := r.cleanupManifestCRs(ctx, kyma); err != nil { - r.Metrics.RecordRequeueReason(metrics.CleanupManifestCrs, queue.UnexpectedRequeue) - return ctrl.Result{}, err + return fmt.Errorf("failed to update module statuses: %w", err) } - r.cleanupMetrics(kyma.Name) - controllerutil.RemoveFinalizer(kyma, shared.KymaFinalizer) - - if err := r.updateKyma(ctx, kyma); err != nil { - r.Metrics.RecordRequeueReason(metrics.KymaDeletion, queue.UnexpectedRequeue) - return ctrl.Result{}, err + // If module get removed from kyma, the module deletion happens here. + if err := r.DeleteNoLongerExistingModules(ctx, kyma); err != nil { + return fmt.Errorf("error while syncing conditions during deleting non exists modules: %w", err) } - r.Metrics.RecordRequeueReason(metrics.KymaDeletion, queue.IntendedRequeue) - return ctrl.Result{Requeue: true}, nil -} - -func (r *Reconciler) cleanupMetrics(kymaName string) { - r.Metrics.CleanupMetrics(kymaName) + return nil } -func (r *Reconciler) cleanupManifestCRs(ctx context.Context, kyma *v1beta2.Kyma) error { - relatedManifests, err := r.getRelatedManifestCRs(ctx, kyma) - if err != nil { - return fmt.Errorf("error while trying to get manifests: %w", err) +// Helper methods for installation controller +func (r *InstallationReconciler) ValidateDefaultChannel(kyma *v1beta2.Kyma) error { + if shared.NoneChannel.Equals(kyma.Spec.Channel) { + return fmt.Errorf("%w: value \"none\" is not allowed in spec.channel", ErrInvalidKymaSpec) } + return nil +} - if r.relatedManifestCRsAreDeleted(relatedManifests) { +func (r *InstallationReconciler) DeleteNoLongerExistingModules(ctx context.Context, kyma *v1beta2.Kyma) error { + moduleStatus := kyma.GetNoLongerExistingModuleStatus() + var err error + if len(moduleStatus) == 0 { return nil } - - if err = r.deleteManifests(ctx, relatedManifests); err != nil { - return fmt.Errorf("error while trying to delete manifests: %w", err) + for i := range moduleStatus { + moduleStatus := moduleStatus[i] + if moduleStatus.Manifest == nil { + continue + } + err = r.deleteManifest(ctx, moduleStatus.Manifest) } - return ErrManifestsStillExist -} -func (r *Reconciler) deleteManifests(ctx context.Context, manifests []v1beta2.Manifest) error { - for i := range manifests { - if err := r.Delete(ctx, &manifests[i]); client.IgnoreNotFound(err) != nil { - return fmt.Errorf("error while trying to delete manifest: %w", err) - } + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("error deleting module %w", err) } return nil } -func (r *Reconciler) getRelatedManifestCRs(ctx context.Context, kyma *v1beta2.Kyma) ([]v1beta2.Manifest, error) { - manifestList := &v1beta2.ManifestList{} - labelSelector := k8slabels.SelectorFromSet(k8slabels.Set{shared.KymaName: kyma.Name}) - if err := r.List(ctx, manifestList, - &client.ListOptions{LabelSelector: labelSelector}); client.IgnoreNotFound(err) != nil { - return nil, fmt.Errorf("failed to get related manifests, %w", err) +func (r *InstallationReconciler) UpdateMetrics(ctx context.Context, kyma *v1beta2.Kyma) { + if err := r.Metrics.UpdateAll(kyma); err != nil { + if metrics.IsMissingMetricsAnnotationOrLabel(err) { + r.Event.Warning(kyma, metricsError, err) + } + logf.FromContext(ctx).V(log.DebugLevel).Info(fmt.Sprintf("error occurred while updating all metrics: %s", err)) } - - return manifestList.Items, nil } -func (r *Reconciler) relatedManifestCRsAreDeleted(manifests []v1beta2.Manifest) bool { - return len(manifests) == 0 +func (r *InstallationReconciler) WatcherEnabled() bool { + return r.SKRWebhookManager != nil } -func (r *Reconciler) removeAllFinalizers(kyma *v1beta2.Kyma) { - for _, finalizer := range kyma.Finalizers { - controllerutil.RemoveFinalizer(kyma, finalizer) - } +func (r *InstallationReconciler) IsKymaManaged() bool { + return r.IsManagedKyma } -func (r *Reconciler) updateKyma(ctx context.Context, kyma *v1beta2.Kyma) error { - if err := r.Update(ctx, kyma); err != nil { - err = fmt.Errorf("error while updating kyma during deletion: %w", err) - r.Event.Warning(kyma, updateSpecError, err) - return err +func (r *InstallationReconciler) GetModuleTemplateList(ctx context.Context) (*v1beta2.ModuleTemplateList, error) { + moduleTemplateList := &v1beta2.ModuleTemplateList{} + if err := r.List(ctx, moduleTemplateList, &client.ListOptions{}); err != nil { + return nil, fmt.Errorf("could not aggregate module templates for module catalog sync: %w", err) } - return nil + return moduleTemplateList, nil } -func (r *Reconciler) reconcileManifests(ctx context.Context, kyma *v1beta2.Kyma) error { - templates := r.TemplateLookup.GetRegularTemplates(ctx, kyma) - prsr := parser.NewParser(r.Client, r.DescriptorProvider, r.RemoteSyncNamespace) - modules := prsr.GenerateModulesFromTemplates(kyma, templates) - - runner := sync.New(r) - if err := runner.ReconcileManifests(ctx, kyma, modules); err != nil { - return fmt.Errorf("sync failed: %w", err) - } - - err := r.ModulesStatusHandler.UpdateModuleStatuses(ctx, kyma, modules) +func (r *InstallationReconciler) UpdateModuleTemplatesIfNeeded(ctx context.Context) error { + moduleTemplateList, err := r.GetModuleTemplateList(ctx) if err != nil { - return fmt.Errorf("failed to update module statuses: %w", err) + return err } - // If module get removed from kyma, the module deletion happens here. - if err := r.DeleteNoLongerExistingModules(ctx, kyma); err != nil { - return fmt.Errorf("error while syncing conditions during deleting non exists modules: %w", err) + for _, mt := range moduleTemplateList.Items { + if needUpdateForMandatoryModuleLabel(mt) { + if err = r.Update(ctx, &mt); err != nil { + return fmt.Errorf("failed to update ModuleTemplate, %w", err) + } + } } + return nil } -func (r *Reconciler) updateStatus(ctx context.Context, kyma *v1beta2.Kyma, +func (r *InstallationReconciler) requeueWithError(ctx context.Context, + kyma *v1beta2.Kyma, + err error, +) (ctrl.Result, error) { + return ctrl.Result{RequeueAfter: r.RequeueIntervals.Busy}, r.updateStatusWithError(ctx, kyma, err) +} + +func (r *InstallationReconciler) updateStatus(ctx context.Context, kyma *v1beta2.Kyma, state shared.State, message string, ) error { if err := status.Helper(r).UpdateStatusForExistingModules(ctx, kyma, state, message); err != nil { @@ -614,7 +429,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, kyma *v1beta2.Kyma, return nil } -func (r *Reconciler) updateStatusWithError(ctx context.Context, kyma *v1beta2.Kyma, err error) error { +func (r *InstallationReconciler) updateStatusWithError(ctx context.Context, kyma *v1beta2.Kyma, err error) error { if err := status.Helper(r).UpdateStatusForExistingModules(ctx, kyma, shared.StateError, err.Error()); err != nil { r.Event.Warning(kyma, updateStatusError, err) return fmt.Errorf("error while updating status to %s: %w", shared.StateError, err) @@ -622,7 +437,7 @@ func (r *Reconciler) updateStatusWithError(ctx context.Context, kyma *v1beta2.Ky return nil } -func (r *Reconciler) deleteManifest(ctx context.Context, trackedManifest *v1beta2.TrackingObject) error { +func (r *InstallationReconciler) deleteManifest(ctx context.Context, trackedManifest *v1beta2.TrackingObject) error { manifest := apimetav1.PartialObjectMetadata{} manifest.SetGroupVersionKind(trackedManifest.GroupVersionKind()) manifest.SetNamespace(trackedManifest.GetNamespace()) @@ -634,46 +449,3 @@ func (r *Reconciler) deleteManifest(ctx context.Context, trackedManifest *v1beta } return nil } - -func (r *Reconciler) deleteOrphanedCertificate(ctx context.Context, kymaName string) error { - if r.WatcherEnabled() { - if err := r.SKRWebhookManager.RemoveSkrCertificate(ctx, kymaName); err != nil { - return err - } - } - return nil -} - -func needUpdateForMandatoryModuleLabel(moduleTemplate v1beta2.ModuleTemplate) bool { - if moduleTemplate.Labels == nil { - moduleTemplate.Labels = make(map[string]string) - } - - if moduleTemplate.Spec.Mandatory { - if moduleTemplate.Labels[shared.IsMandatoryModule] == shared.EnableLabelValue { - return false - } - - moduleTemplate.Labels[shared.IsMandatoryModule] = shared.EnableLabelValue - return true - } - - if !moduleTemplate.Spec.Mandatory { - if moduleTemplate.Labels[shared.IsMandatoryModule] == shared.EnableLabelValue { - delete(moduleTemplate.Labels, shared.IsMandatoryModule) - return true - } - } - - return false -} - -func setModuleStatusesToError(kyma *v1beta2.Kyma, message string) { - moduleStatuses := kyma.Status.Modules - for i := range moduleStatuses { - moduleStatuses[i].State = shared.StateError - if message != "" { - moduleStatuses[i].Message = message - } - } -} diff --git a/internal/controller/kyma/setup.go b/internal/controller/kyma/setup.go index 0b605b658c..78d173fd00 100644 --- a/internal/controller/kyma/setup.go +++ b/internal/controller/kyma/setup.go @@ -41,7 +41,11 @@ var ( errConvertingWatcherEvent = errors.New("error converting watched object to unstructured event") ) -func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options, settings SetupOptions) error { +// SetupWithManager sets up the InstallationReconciler with the Manager +func (r *InstallationReconciler) SetupWithManager(mgr ctrl.Manager, + opts ctrlruntime.Options, + settings SetupOptions, +) error { var verifyFunc watcherevent.Verify if settings.EnableDomainNameVerification { verifyFunc = security.NewRequestVerifier(mgr.GetClient()).Verify @@ -57,11 +61,11 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options ) if err := mgr.Add(runnableListener); err != nil { - return fmt.Errorf("KymaReconciler %w", err) + return fmt.Errorf("InstallationReconciler %w", err) } if err := ctrl.NewControllerManagedBy(mgr).For(&v1beta2.Kyma{}). - Named(controllerName). + Named(controllerName+"-installation"). WithOptions(opts). WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{})). Watches(&v1beta2.ModuleTemplate{}, @@ -73,13 +77,26 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options handler.OnlyControllerOwner()), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). WatchesRawSource(source.Channel(controller.AdaptEvents(runnableListener.ReceivedEvents), r.skrEventHandler())). Complete(r); err != nil { - return fmt.Errorf("failed to setup manager for kyma controller: %w", err) + return fmt.Errorf("failed to setup manager for kyma installation controller: %w", err) } return nil } -func (r *Reconciler) skrEventHandler() *handler.Funcs { +// SetupWithManager sets up the DeletionReconciler with the Manager +func (r *DeletionReconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options) error { + if err := ctrl.NewControllerManagedBy(mgr).For(&v1beta2.Kyma{}). + Named(controllerName + "-deletion"). + WithOptions(opts). + Complete(r); err != nil { + return fmt.Errorf("failed to setup manager for kyma deletion controller: %w", err) + } + + return nil +} + +// skrEventHandler creates event handler for InstallationReconciler +func (r *InstallationReconciler) skrEventHandler() *handler.Funcs { return &handler.Funcs{ GenericFunc: func(ctx context.Context, evnt event.GenericEvent, queue workqueue.TypedRateLimitingInterface[ctrl.Request], diff --git a/internal/controller/kyma/shared_utils.go b/internal/controller/kyma/shared_utils.go new file mode 100644 index 0000000000..768a631bec --- /dev/null +++ b/internal/controller/kyma/shared_utils.go @@ -0,0 +1,104 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kyma + +import ( + "context" + "errors" + + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/event" + "github.com/kyma-project/lifecycle-manager/internal/remote" + modulecommon "github.com/kyma-project/lifecycle-manager/pkg/module/common" + "github.com/kyma-project/lifecycle-manager/pkg/watcher" +) + +var ( + ErrManifestsStillExist = errors.New("manifests still exist") + ErrInvalidKymaSpec = errors.New("invalid kyma spec") + ErrKymaInErrorState = errors.New("kyma in error state") +) + +const ( + metricsError event.Reason = "MetricsError" + updateSpecError event.Reason = "UpdateSpecError" + updateStatusError event.Reason = "UpdateStatusError" + patchStatusError event.Reason = "PatchStatus" +) + +type SKRWebhookManager interface { + Reconcile(ctx context.Context, kyma *v1beta2.Kyma) error + Remove(ctx context.Context, kyma *v1beta2.Kyma) error + RemoveSkrCertificate(ctx context.Context, kymaName string) error +} + +type ModuleStatusHandler interface { + UpdateModuleStatuses(ctx context.Context, kyma *v1beta2.Kyma, modules modulecommon.Modules) error +} + +// checkSKRWebhookReadiness is a shared function used by both controllers +func checkSKRWebhookReadiness(ctx context.Context, skrClient *remote.SkrContext, kyma *v1beta2.Kyma) error { + err := watcher.AssertDeploymentReady(ctx, skrClient) + if err != nil { + kyma.UpdateCondition(v1beta2.ConditionTypeSKRWebhook, apimetav1.ConditionFalse) + if errors.Is(err, watcher.ErrSkrWebhookDeploymentInBackoff) { + return err + } + return nil + } + kyma.UpdateCondition(v1beta2.ConditionTypeSKRWebhook, apimetav1.ConditionTrue) + return nil +} + +// needUpdateForMandatoryModuleLabel checks if a module template needs updating for mandatory module labels +func needUpdateForMandatoryModuleLabel(moduleTemplate v1beta2.ModuleTemplate) bool { + if moduleTemplate.Labels == nil { + moduleTemplate.Labels = make(map[string]string) + } + + if moduleTemplate.Spec.Mandatory { + if moduleTemplate.Labels[shared.IsMandatoryModule] == shared.EnableLabelValue { + return false + } + + moduleTemplate.Labels[shared.IsMandatoryModule] = shared.EnableLabelValue + return true + } + + if !moduleTemplate.Spec.Mandatory { + if moduleTemplate.Labels[shared.IsMandatoryModule] == shared.EnableLabelValue { + delete(moduleTemplate.Labels, shared.IsMandatoryModule) + return true + } + } + + return false +} + +// setModuleStatusesToError sets all module statuses to error state +func setModuleStatusesToError(kyma *v1beta2.Kyma, message string) { + moduleStatuses := kyma.Status.Modules + for i := range moduleStatuses { + moduleStatuses[i].State = shared.StateError + if message != "" { + moduleStatuses[i].Message = message + } + } +} diff --git a/tests/integration/controller/kcp/suite_test.go b/tests/integration/controller/kcp/suite_test.go index 153584f580..c7361b79cd 100644 --- a/tests/integration/controller/kcp/suite_test.go +++ b/tests/integration/controller/kcp/suite_test.go @@ -58,9 +58,10 @@ import ( _ "ocm.software/ocm/api/ocm" - . "github.com/kyma-project/lifecycle-manager/pkg/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -153,7 +154,9 @@ var _ = BeforeSuite(func() { crdCache = crd.NewCache(nil) noOpMetricsFunc := func(kymaName, moduleName string) {} moduleStatusGen := generator.NewModuleStatusGenerator(fromerror.GenerateModuleStatusFromError) - err = (&kyma.Reconciler{ + kymaMetrics := metrics.NewKymaMetrics(metrics.NewSharedMetrics()) + // Setup InstallationReconciler for KCP tests + err = (&kyma.InstallationReconciler{ Client: kcpClient, SkrContextFactory: testSkrContextFactory, Event: testEventRec, @@ -163,7 +166,7 @@ var _ = BeforeSuite(func() { ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, IsManagedKyma: true, - Metrics: metrics.NewKymaMetrics(metrics.NewSharedMetrics()), + Metrics: kymaMetrics, RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, flags.DefaultRemoteSyncNamespace), TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, @@ -176,6 +179,31 @@ var _ = BeforeSuite(func() { }).SetupWithManager(mgr, ctrlruntime.Options{}, kyma.SetupOptions{ListenerAddr: UseRandomPort}) Expect(err).ToNot(HaveOccurred()) + + // Setup DeletionReconciler for KCP tests + err = (&kyma.DeletionReconciler{ + Client: kcpClient, + SkrContextFactory: testSkrContextFactory, + Event: testEventRec, + RequeueIntervals: intervals, + DescriptorProvider: descriptorProvider, + SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, testSkrContextFactory, crdCache), + ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), + RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, + IsManagedKyma: true, + Metrics: kymaMetrics, + RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, + flags.DefaultRemoteSyncNamespace), + TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, + moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( + []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ + moduletemplateinfolookup.NewByVersionStrategy(kcpClient), + moduletemplateinfolookup.NewByChannelStrategy(kcpClient), + moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(kcpClient), + })), + }).SetupWithManager(mgr, ctrlruntime.Options{}) // DeletionReconciler doesn't need SKR event listener + Expect(err).ToNot(HaveOccurred()) + Eventually(CreateNamespace, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, ControlPlaneNamespace).Should(Succeed()) diff --git a/tests/integration/controller/kyma/suite_test.go b/tests/integration/controller/kyma/suite_test.go index 88a8a3fe8b..05e9a33f9f 100644 --- a/tests/integration/controller/kyma/suite_test.go +++ b/tests/integration/controller/kyma/suite_test.go @@ -58,9 +58,10 @@ import ( _ "ocm.software/ocm/api/ocm" - . "github.com/kyma-project/lifecycle-manager/pkg/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -148,7 +149,9 @@ var _ = BeforeSuite(func() { testSkrContextFactory = testskrcontext.NewDualClusterFactory(kcpClient.Scheme(), testEventRec) noOpMetricsFunc := func(kymaName, moduleName string) {} moduleStatusGen := generator.NewModuleStatusGenerator(fromerror.GenerateModuleStatusFromError) - err = (&kyma.Reconciler{ + kymaMetrics := metrics.NewKymaMetrics(metrics.NewSharedMetrics()) + // Setup InstallationReconciler for handling Kyma installation and updates + err = (&kyma.InstallationReconciler{ Client: kcpClient, Event: testEventRec, DescriptorProvider: descriptorProvider, @@ -160,7 +163,7 @@ var _ = BeforeSuite(func() { RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, flags.DefaultRemoteSyncNamespace), RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, - Metrics: metrics.NewKymaMetrics(metrics.NewSharedMetrics()), + Metrics: kymaMetrics, TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ @@ -171,6 +174,31 @@ var _ = BeforeSuite(func() { }).SetupWithManager(mgr, ctrlruntime.Options{}, kyma.SetupOptions{ListenerAddr: randomPort}) Expect(err).ToNot(HaveOccurred()) + + // Setup DeletionReconciler for handling Kyma deletion + err = (&kyma.DeletionReconciler{ + Client: kcpClient, + Event: testEventRec, + DescriptorProvider: descriptorProvider, + SkrContextFactory: testSkrContextFactory, + SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, testSkrContextFactory, crd.NewCache(nil)), + ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), + RequeueIntervals: intervals, + IsManagedKyma: true, + RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, + flags.DefaultRemoteSyncNamespace), + RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, + Metrics: kymaMetrics, + TemplateLookup: templatelookup.NewTemplateLookup(kcpClient, descriptorProvider, + moduletemplateinfolookup.NewModuleTemplateInfoLookupStrategies( + []moduletemplateinfolookup.ModuleTemplateInfoLookupStrategy{ + moduletemplateinfolookup.NewByVersionStrategy(kcpClient), + moduletemplateinfolookup.NewByChannelStrategy(kcpClient), + moduletemplateinfolookup.NewByModuleReleaseMetaStrategy(kcpClient), + })), + }).SetupWithManager(mgr, ctrlruntime.Options{}) + Expect(err).ToNot(HaveOccurred()) + Eventually(CreateNamespace, Timeout, Interval). WithContext(ctx). WithArguments(kcpClient, ControlPlaneNamespace).Should(Succeed()) diff --git a/tests/integration/controller/withwatcher/suite_test.go b/tests/integration/controller/withwatcher/suite_test.go index 4a8aeda10b..b4c85c3e6f 100644 --- a/tests/integration/controller/withwatcher/suite_test.go +++ b/tests/integration/controller/withwatcher/suite_test.go @@ -69,9 +69,10 @@ import ( "github.com/kyma-project/lifecycle-manager/tests/integration" testskrcontext "github.com/kyma-project/lifecycle-manager/tests/integration/commontestutils/skrcontextimpl" - . "github.com/kyma-project/lifecycle-manager/pkg/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + . "github.com/kyma-project/lifecycle-manager/pkg/testutils" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -234,7 +235,9 @@ var _ = BeforeSuite(func() { noOpMetricsFunc := func(kymaName, moduleName string) {} moduleStatusGen := generator.NewModuleStatusGenerator(fromerror.GenerateModuleStatusFromError) - err = (&kyma.Reconciler{ + kymaMetrics := metrics.NewKymaMetrics(metrics.NewSharedMetrics()) + // Setup InstallationReconciler for withwatcher tests + err = (&kyma.InstallationReconciler{ Client: kcpClient, SkrContextFactory: testSkrContextFactory, Event: testEventRec, @@ -244,12 +247,29 @@ var _ = BeforeSuite(func() { SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, testSkrContextFactory, nil), ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, - Metrics: metrics.NewKymaMetrics(metrics.NewSharedMetrics()), + Metrics: kymaMetrics, RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, flags.DefaultRemoteSyncNamespace), }).SetupWithManager(mgr, ctrlruntime.Options{}, kyma.SetupOptions{ListenerAddr: listenerAddr}) Expect(err).ToNot(HaveOccurred()) + // Setup DeletionReconciler for withwatcher tests + err = (&kyma.DeletionReconciler{ + Client: kcpClient, + SkrContextFactory: testSkrContextFactory, + Event: testEventRec, + RequeueIntervals: intervals, + SKRWebhookManager: skrWebhookChartManager, + DescriptorProvider: provider.NewCachedDescriptorProvider(), + SyncRemoteCrds: remote.NewSyncCrdsUseCase(kcpClient, testSkrContextFactory, nil), + ModulesStatusHandler: modules.NewStatusHandler(moduleStatusGen, kcpClient, noOpMetricsFunc), + RemoteSyncNamespace: flags.DefaultRemoteSyncNamespace, + Metrics: kymaMetrics, + RemoteCatalog: remote.NewRemoteCatalogFromKyma(kcpClient, testSkrContextFactory, + flags.DefaultRemoteSyncNamespace), + }).SetupWithManager(mgr, ctrlruntime.Options{}) // DeletionReconciler doesn't need SKR event listener + Expect(err).ToNot(HaveOccurred()) + err = (&watcherctrl.Reconciler{ Client: mgr.GetClient(), RestConfig: mgr.GetConfig(),