diff --git a/internal/controller/datadogagent/controller.go b/internal/controller/datadogagent/controller.go index 57d728a7d8..943985cf13 100644 --- a/internal/controller/datadogagent/controller.go +++ b/internal/controller/datadogagent/controller.go @@ -64,7 +64,8 @@ import ( ) const ( - defaultRequeuePeriod = 15 * time.Second + defaultRequeuePeriod = 15 * time.Second + defaultErrRequeuePeriod = 5 * time.Second ) // ReconcilerOptions provides options read from command line diff --git a/internal/controller/datadogagent/controller_reconcile_v2.go b/internal/controller/datadogagent/controller_reconcile_v2.go index 12c4d9d24c..f11b748ee4 100644 --- a/internal/controller/datadogagent/controller_reconcile_v2.go +++ b/internal/controller/datadogagent/controller_reconcile_v2.go @@ -22,6 +22,7 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/defaults" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/agentprofile" "github.com/DataDog/datadog-operator/pkg/condition" "github.com/DataDog/datadog-operator/pkg/controller/utils" @@ -40,7 +41,8 @@ func (r *Reconciler) internalReconcileV2(ctx context.Context, instance *datadogh } // 2. Handle finalizer logic. - if result, err := r.handleFinalizer(reqLogger, instance, r.finalizeDadV2); utils.ShouldReturn(result, err) { + final := finalizer.NewFinalizer(reqLogger, r.client, r.deleteResource(reqLogger), defaultErrRequeuePeriod) + if result, err := final.HandleFinalizer(ctx, instance, "", datadogAgentFinalizer); utils.ShouldReturn(result, err) { return result, err } diff --git a/internal/controller/datadogagent/finalizer.go b/internal/controller/datadogagent/finalizer.go index 44d7745a0d..dad9e3aa14 100644 --- a/internal/controller/datadogagent/finalizer.go +++ b/internal/controller/datadogagent/finalizer.go @@ -12,11 +12,10 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/store" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/agentprofile" "github.com/DataDog/datadog-operator/pkg/constants" "github.com/DataDog/datadog-operator/pkg/kubernetes" @@ -26,41 +25,10 @@ const ( datadogAgentFinalizer = "finalizer.agent.datadoghq.com" ) -type finalizerDadFunc func(reqLogger logr.Logger, dda client.Object) error - -func (r *Reconciler) handleFinalizer(reqLogger logr.Logger, dda client.Object, finalizerDad finalizerDadFunc) (reconcile.Result, error) { - // Check if the DatadogAgent instance is marked to be deleted, which is - // indicated by the deletion timestamp being set. - isDadMarkedToBeDeleted := dda.GetDeletionTimestamp() != nil - if isDadMarkedToBeDeleted { - if controllerutil.ContainsFinalizer(dda, datadogAgentFinalizer) { - // Run finalization logic for datadogAgentFinalizer. If the - // finalization logic fails, don't remove the finalizer so - // that we can retry during the next reconciliation. - if err := finalizerDad(reqLogger, dda); err != nil { - return reconcile.Result{}, err - } - - // Remove datadogAgentFinalizer. Once all finalizers have been - // removed, the object will be deleted. - controllerutil.RemoveFinalizer(dda, datadogAgentFinalizer) - err := r.client.Update(context.TODO(), dda) - if err != nil { - return reconcile.Result{}, err - } - } - return reconcile.Result{Requeue: true}, nil - } - - // Add finalizer for this CR - if !controllerutil.ContainsFinalizer(dda, datadogAgentFinalizer) { - if err := r.addFinalizer(reqLogger, dda); err != nil { - return reconcile.Result{}, err - } - return reconcile.Result{Requeue: true}, nil +func (r *Reconciler) deleteResource(reqLogger logr.Logger) finalizer.ResourceDeleteFunc { + return func(ctx context.Context, k8sObj client.Object, datadogID string) error { + return r.finalizeDadV2(reqLogger, k8sObj) } - - return reconcile.Result{}, nil } func (r *Reconciler) finalizeDadV2(reqLogger logr.Logger, obj client.Object) error { @@ -82,19 +50,6 @@ func (r *Reconciler) finalizeDadV2(reqLogger logr.Logger, obj client.Object) err return nil } -func (r *Reconciler) addFinalizer(reqLogger logr.Logger, dda client.Object) error { - reqLogger.Info("Adding Finalizer for the DatadogAgent") - controllerutil.AddFinalizer(dda, datadogAgentFinalizer) - - // Update CR - err := r.client.Update(context.TODO(), dda) - if err != nil { - reqLogger.Error(err, "Failed to update DatadogAgent with finalizer") - return err - } - return nil -} - // profilesCleanup performs the cleanups required for the profiles feature. The // only thing that we need to do is to ensure that no nodes are left with the // profile label. diff --git a/internal/controller/datadogagent/finalizer_test.go b/internal/controller/datadogagent/finalizer_test.go index 9bfb3b78fd..1654145c82 100644 --- a/internal/controller/datadogagent/finalizer_test.go +++ b/internal/controller/datadogagent/finalizer_test.go @@ -139,7 +139,7 @@ func Test_handleFinalizer(t *testing.T) { reconciler := reconcilerForFinalizerTest(initialKubeObjects) - _, err := reconciler.handleFinalizer(logf.Log.WithName("Handle Finalizer V2 test"), dda, reconciler.finalizeDadV2) + err := reconciler.finalizeDadV2(logf.Log.WithName("Handle Finalizer V2 test"), dda) assert.NoError(t, err) // Check that the cluster roles associated with the Datadog Agent have been deleted diff --git a/internal/controller/datadogagentinternal/controller.go b/internal/controller/datadogagentinternal/controller.go index fc6c04206c..25aebff4d4 100644 --- a/internal/controller/datadogagentinternal/controller.go +++ b/internal/controller/datadogagentinternal/controller.go @@ -58,7 +58,8 @@ import ( ) const ( - defaultRequeuePeriod = 15 * time.Second + defaultRequeuePeriod = 15 * time.Second + defaultErrRequeuePeriod = 5 * time.Second ) // ReconcilerOptions provides options read from command line diff --git a/internal/controller/datadogagentinternal/controller_reconcile_v2.go b/internal/controller/datadogagentinternal/controller_reconcile_v2.go index ee806fbfdc..2fef68db6b 100644 --- a/internal/controller/datadogagentinternal/controller_reconcile_v2.go +++ b/internal/controller/datadogagentinternal/controller_reconcile_v2.go @@ -18,7 +18,9 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/defaults" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/condition" + "github.com/DataDog/datadog-operator/pkg/constants" "github.com/DataDog/datadog-operator/pkg/controller/utils" ) @@ -34,7 +36,8 @@ func (r *Reconciler) internalReconcileV2(ctx context.Context, instance *v1alpha1 // } // 2. Handle finalizer logic. - if result, err := r.handleFinalizer(ctx, instance, r.finalizeDDAI); utils.ShouldReturn(result, err) { + final := finalizer.NewFinalizer(logger, r.client, r.deleteResource(), defaultErrRequeuePeriod) + if result, err := final.HandleFinalizer(ctx, instance, "", constants.DatadogAgentInternalFinalizer); utils.ShouldReturn(result, err) { return result, err } diff --git a/internal/controller/datadogagentinternal/finalizer.go b/internal/controller/datadogagentinternal/finalizer.go index 6a25da304a..8949d0acd9 100644 --- a/internal/controller/datadogagentinternal/finalizer.go +++ b/internal/controller/datadogagentinternal/finalizer.go @@ -12,51 +12,19 @@ import ( "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/store" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/agentprofile" "github.com/DataDog/datadog-operator/pkg/constants" "github.com/DataDog/datadog-operator/pkg/kubernetes" ) -type finalizerDDAIFunc func(ctx context.Context, dda client.Object) error - -func (r *Reconciler) handleFinalizer(ctx context.Context, ddai client.Object, finalizerDDAI finalizerDDAIFunc) (reconcile.Result, error) { - // Check if the DatadogAgentInternal instance is marked to be deleted, which is - // indicated by the deletion timestamp being set. - isDDAIMarkedToBeDeleted := ddai.GetDeletionTimestamp() != nil - if isDDAIMarkedToBeDeleted { - if controllerutil.ContainsFinalizer(ddai, constants.DatadogAgentInternalFinalizer) { - // Run finalization logic for datadogAgentFinalizer. If the - // finalization logic fails, don't remove the finalizer so - // that we can retry during the next reconciliation. - if err := finalizerDDAI(ctx, ddai); err != nil { - return reconcile.Result{}, err - } - - // Remove datadogAgentFinalizer. Once all finalizers have been - // removed, the object will be deleted. - controllerutil.RemoveFinalizer(ddai, constants.DatadogAgentInternalFinalizer) - err := r.client.Update(ctx, ddai) - if err != nil { - return reconcile.Result{}, err - } - } - return reconcile.Result{Requeue: true}, nil +func (r *Reconciler) deleteResource() finalizer.ResourceDeleteFunc { + return func(ctx context.Context, k8sObj client.Object, datadogID string) error { + return r.finalizeDDAI(ctx, k8sObj) } - - // Add finalizer for this CR - if !controllerutil.ContainsFinalizer(ddai, constants.DatadogAgentInternalFinalizer) { - if err := r.addFinalizer(ctx, ddai); err != nil { - return reconcile.Result{}, err - } - return reconcile.Result{Requeue: true}, nil - } - - return reconcile.Result{}, nil } func (r *Reconciler) finalizeDDAI(ctx context.Context, obj client.Object) error { @@ -79,20 +47,6 @@ func (r *Reconciler) finalizeDDAI(ctx context.Context, obj client.Object) error return nil } -func (r *Reconciler) addFinalizer(ctx context.Context, ddai client.Object) error { - logger := ctrl.LoggerFrom(ctx) - logger.Info("Adding Finalizer for the DatadogAgentInternal") - controllerutil.AddFinalizer(ddai, constants.DatadogAgentInternalFinalizer) - - // Update CR - err := r.client.Update(ctx, ddai) - if err != nil { - logger.Error(err, "Failed to update DatadogAgentInternal with finalizer") - return err - } - return nil -} - // profilesCleanup performs the cleanups required for the profiles feature. The // only thing that we need to do is to ensure that no nodes are left with the // profile label. diff --git a/internal/controller/datadogagentinternal/finalizer_test.go b/internal/controller/datadogagentinternal/finalizer_test.go index b5d0dbe2d9..11e0ee7fa9 100644 --- a/internal/controller/datadogagentinternal/finalizer_test.go +++ b/internal/controller/datadogagentinternal/finalizer_test.go @@ -128,7 +128,7 @@ func Test_handleFinalizer(t *testing.T) { reconciler := reconcilerForFinalizerTest(initialKubeObjects) - _, err := reconciler.handleFinalizer(context.Background(), ddai, reconciler.finalizeDDAI) + err := reconciler.finalizeDDAI(context.Background(), ddai) assert.NoError(t, err) // Check that the cluster roles associated with the Datadog Agent have been deleted diff --git a/internal/controller/datadogdashboard/controller.go b/internal/controller/datadogdashboard/controller.go index b1c8e1b8c9..3927ca3477 100644 --- a/internal/controller/datadogdashboard/controller.go +++ b/internal/controller/datadogdashboard/controller.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/internal/controller/utils" "github.com/DataDog/datadog-operator/pkg/config" ctrutils "github.com/DataDog/datadog-operator/pkg/controller/utils" @@ -111,7 +112,8 @@ func (r *Reconciler) internalReconcile(ctx context.Context, req reconcile.Reques return ctrl.Result{}, err } - if result, err = r.handleFinalizer(logger, instance); ctrutils.ShouldReturn(result, err) { + final := finalizer.NewFinalizer(logger, r.client, r.deleteResource(logger), defaultErrRequeuePeriod) + if result, err = final.HandleFinalizer(ctx, instance, instance.Status.ID, datadogDashboardFinalizer); ctrutils.ShouldReturn(result, err) { return result, err } diff --git a/internal/controller/datadogdashboard/finalizer.go b/internal/controller/datadogdashboard/finalizer.go index 143a99b2db..ea3473bc2a 100644 --- a/internal/controller/datadogdashboard/finalizer.go +++ b/internal/controller/datadogdashboard/finalizer.go @@ -10,10 +10,9 @@ import ( "fmt" "github.com/go-logr/logr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/client" - datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" ) @@ -21,58 +20,18 @@ const ( datadogDashboardFinalizer = "finalizer.datadoghq.com/dashboard" ) -func (r *Reconciler) handleFinalizer(logger logr.Logger, db *datadoghqv1alpha1.DatadogDashboard) (ctrl.Result, error) { - // Check if the DatadogDashboard instance is marked to be deleted, which is indicated by the deletion timestamp being set. - if db.GetDeletionTimestamp() != nil { - if controllerutil.ContainsFinalizer(db, datadogDashboardFinalizer) { - r.finalizeDatadogDashboard(logger, db) - - controllerutil.RemoveFinalizer(db, datadogDashboardFinalizer) - err := r.client.Update(context.TODO(), db) +func (r *Reconciler) deleteResource(logger logr.Logger) finalizer.ResourceDeleteFunc { + return func(ctx context.Context, k8sObj client.Object, datadogID string) error { + if datadogID != "" { + err := deleteDashboard(r.datadogAuth, r.datadogClient, datadogID) if err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + logger.Error(err, "failed to finalize dashboard", "dashboard ID", fmt.Sprint(datadogID)) + return nil } + logger.Info("Successfully finalized DatadogDashboard", "dashboard ID", fmt.Sprint(datadogID)) } - - // Requeue until the object is properly deleted by Kubernetes - return ctrl.Result{RequeueAfter: defaultRequeuePeriod}, nil + event := buildEventInfo(k8sObj.GetName(), k8sObj.GetNamespace(), datadog.DeletionEvent) + r.recordEvent(k8sObj, event) + return nil } - - // Add finalizer for this resource if it doesn't already exist. - if !controllerutil.ContainsFinalizer(db, datadogDashboardFinalizer) { - if err := r.addFinalizer(logger, db); err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err - } - - return ctrl.Result{Requeue: true}, nil - } - - // Proceed in reconcile loop. - return ctrl.Result{}, nil -} - -func (r *Reconciler) finalizeDatadogDashboard(logger logr.Logger, db *datadoghqv1alpha1.DatadogDashboard) { - err := deleteDashboard(r.datadogAuth, r.datadogClient, db.Status.ID) - if err != nil { - logger.Error(err, "failed to finalize dashboard", "dashboard ID", fmt.Sprint(db.Status.ID)) - - return - } - logger.Info("Successfully finalized DatadogDashboard", "dashboard ID", fmt.Sprint(db.Status.ID)) - event := buildEventInfo(db.Name, db.Namespace, datadog.DeletionEvent) - r.recordEvent(db, event) -} - -func (r *Reconciler) addFinalizer(logger logr.Logger, db *datadoghqv1alpha1.DatadogDashboard) error { - logger.Info("Adding Finalizer for the DatadogDashboard") - - controllerutil.AddFinalizer(db, datadogDashboardFinalizer) - - err := r.client.Update(context.TODO(), db) - if err != nil { - logger.Error(err, "failed to update DatadogDashboard with finalizer", "dashboard ID", fmt.Sprint(db.Status.ID)) - return err - } - - return nil } diff --git a/internal/controller/datadogdashboard/finalizer_test.go b/internal/controller/datadogdashboard/finalizer_test.go index c5dc133453..e962689493 100644 --- a/internal/controller/datadogdashboard/finalizer_test.go +++ b/internal/controller/datadogdashboard/finalizer_test.go @@ -18,6 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" ) var ( @@ -58,7 +59,8 @@ func Test_handleFinalizer(t *testing.T) { t.Run(test.name, func(t *testing.T) { reqLogger := testLogger.WithValues("test:", test.name) _ = r.client.Create(context.TODO(), test.db) - _, err := r.handleFinalizer(reqLogger, test.db) + final := finalizer.NewFinalizer(reqLogger, r.client, r.deleteResource(reqLogger), defaultErrRequeuePeriod) + _, err := final.HandleFinalizer(context.TODO(), test.db, test.db.Status.ID, datadogDashboardFinalizer) assert.NoError(t, err) if test.finalizerShouldExist { assert.True(t, controllerutil.ContainsFinalizer(test.db, datadogDashboardFinalizer)) diff --git a/internal/controller/datadoggenericresource/controller.go b/internal/controller/datadoggenericresource/controller.go index f807e62791..3af4413646 100644 --- a/internal/controller/datadoggenericresource/controller.go +++ b/internal/controller/datadoggenericresource/controller.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/internal/controller/utils" "github.com/DataDog/datadog-operator/pkg/config" ctrutils "github.com/DataDog/datadog-operator/pkg/controller/utils" @@ -108,7 +109,8 @@ func (r *Reconciler) internalReconcile(ctx context.Context, req reconcile.Reques return ctrl.Result{}, err } - if result, err = r.handleFinalizer(logger, instance); ctrutils.ShouldReturn(result, err) { + final := finalizer.NewFinalizer(logger, r.client, r.deleteResource(logger), defaultErrRequeuePeriod) + if result, err = final.HandleFinalizer(ctx, instance, instance.Status.Id, datadogGenericResourceFinalizer); ctrutils.ShouldReturn(result, err) { return result, err } diff --git a/internal/controller/datadoggenericresource/finalizer.go b/internal/controller/datadoggenericresource/finalizer.go index b8ceb719a6..cffb4160ac 100644 --- a/internal/controller/datadoggenericresource/finalizer.go +++ b/internal/controller/datadoggenericresource/finalizer.go @@ -10,10 +10,10 @@ import ( "fmt" "github.com/go-logr/logr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/client" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" ) @@ -21,58 +21,20 @@ const ( datadogGenericResourceFinalizer = "finalizer.datadoghq.com/genericresource" ) -func (r *Reconciler) handleFinalizer(logger logr.Logger, instance *datadoghqv1alpha1.DatadogGenericResource) (ctrl.Result, error) { - // Check if the DatadogGenericResource instance is marked to be deleted, which is indicated by the deletion timestamp being set. - if instance.GetDeletionTimestamp() != nil { - if controllerutil.ContainsFinalizer(instance, datadogGenericResourceFinalizer) { - r.finalizeDatadogCustomResource(logger, instance) - - controllerutil.RemoveFinalizer(instance, datadogGenericResourceFinalizer) - err := r.client.Update(context.TODO(), instance) - if err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err - } +func (r *Reconciler) deleteResource(logger logr.Logger) finalizer.ResourceDeleteFunc { + return func(ctx context.Context, k8sObj client.Object, datadogID string) error { + instance, ok := k8sObj.(*datadoghqv1alpha1.DatadogGenericResource) + if !ok { + return fmt.Errorf("unexpected object type %T", k8sObj) } - - // Requeue until the object is properly deleted by Kubernetes - return ctrl.Result{RequeueAfter: defaultRequeuePeriod}, nil - } - - // Add finalizer for this resource if it doesn't already exist. - if !controllerutil.ContainsFinalizer(instance, datadogGenericResourceFinalizer) { - if err := r.addFinalizer(logger, instance); err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + err := apiDelete(r, instance) + if err != nil { + logger.Error(err, "failed to finalize", "custom resource Id", fmt.Sprint(datadogID)) + return nil } - - return ctrl.Result{Requeue: true}, nil - } - - // Proceed in reconcile loop. - return ctrl.Result{}, nil -} - -func (r *Reconciler) finalizeDatadogCustomResource(logger logr.Logger, instance *datadoghqv1alpha1.DatadogGenericResource) { - err := apiDelete(r, instance) - if err != nil { - logger.Error(err, "failed to finalize ", "custom resource Id", fmt.Sprint(instance.Status.Id)) - - return - } - logger.Info("Successfully finalized DatadogGenericResource", "custom resource Id", fmt.Sprint(instance.Status.Id)) - event := buildEventInfo(instance.Name, instance.Namespace, datadog.DeletionEvent) - r.recordEvent(instance, event) -} - -func (r *Reconciler) addFinalizer(logger logr.Logger, instance *datadoghqv1alpha1.DatadogGenericResource) error { - logger.Info("Adding Finalizer for the DatadogGenericResource") - - controllerutil.AddFinalizer(instance, datadogGenericResourceFinalizer) - - err := r.client.Update(context.TODO(), instance) - if err != nil { - logger.Error(err, "failed to update DatadogGenericResource with finalizer", "custom resource Id", fmt.Sprint(instance.Status.Id)) - return err + logger.Info("Successfully finalized DatadogGenericResource", "custom resource Id", fmt.Sprint(datadogID)) + event := buildEventInfo(k8sObj.GetName(), k8sObj.GetNamespace(), datadog.DeletionEvent) + r.recordEvent(k8sObj, event) + return nil } - - return nil } diff --git a/internal/controller/datadoggenericresource/finalizer_test.go b/internal/controller/datadoggenericresource/finalizer_test.go index 0b22cbb6f0..a388898a05 100644 --- a/internal/controller/datadoggenericresource/finalizer_test.go +++ b/internal/controller/datadoggenericresource/finalizer_test.go @@ -24,6 +24,7 @@ import ( "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" ) const ( @@ -100,7 +101,8 @@ func Test_handleFinalizer(t *testing.T) { testGcr := &datadoghqv1alpha1.DatadogGenericResource{} err := r.client.Get(context.TODO(), client.ObjectKey{Name: test.resourceName, Namespace: testNamespace}, testGcr) - _, err = r.handleFinalizer(reqLogger, testGcr) + final := finalizer.NewFinalizer(reqLogger, r.client, r.deleteResource(reqLogger), defaultErrRequeuePeriod) + _, err = final.HandleFinalizer(context.TODO(), testGcr, testGcr.Status.Id, datadogGenericResourceFinalizer) assert.NoError(t, err) if test.finalizerShouldExist { diff --git a/internal/controller/datadogmonitor/controller.go b/internal/controller/datadogmonitor/controller.go index 1f96e41840..3258a9ce53 100644 --- a/internal/controller/datadogmonitor/controller.go +++ b/internal/controller/datadogmonitor/controller.go @@ -29,6 +29,7 @@ import ( datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" apiutils "github.com/DataDog/datadog-operator/api/utils" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/config" ctrutils "github.com/DataDog/datadog-operator/pkg/controller/utils" "github.com/DataDog/datadog-operator/pkg/controller/utils/comparison" @@ -139,7 +140,8 @@ func (r *Reconciler) internalReconcile(ctx context.Context, instance *datadoghqv newStatus := instance.Status.DeepCopy() - if result, err = r.handleFinalizer(logger, instance); ctrutils.ShouldReturn(result, err) { + final := finalizer.NewFinalizer(logger, r.client, r.deleteResource(logger), defaultErrRequeuePeriod) + if result, err = final.HandleFinalizer(ctx, instance, fmt.Sprint(instance.Status.ID), datadogMonitorFinalizer); ctrutils.ShouldReturn(result, err) { return result, err } diff --git a/internal/controller/datadogmonitor/controller_test.go b/internal/controller/datadogmonitor/controller_test.go index bb405c1150..7feba401ae 100644 --- a/internal/controller/datadogmonitor/controller_test.go +++ b/internal/controller/datadogmonitor/controller_test.go @@ -76,7 +76,7 @@ func TestReconcileDatadogMonitor_Reconcile(t *testing.T) { args: args{ loadFunc: genericDatadogMonitor, }, - wantResult: reconcile.Result{RequeueAfter: defaultRequeuePeriod}, + wantResult: reconcile.Result{Requeue: true}, wantFunc: func(c client.Client) error { dm := &datadoghqv1alpha1.DatadogMonitor{} if err := c.Get(context.TODO(), types.NamespacedName{Name: resourcesName, Namespace: resourcesNamespace}, dm); err != nil { @@ -104,7 +104,7 @@ func TestReconcileDatadogMonitor_Reconcile(t *testing.T) { }, }, { - name: "DatadogMonitor exists, adds required tags before create", + name: "DatadogMonitor exists, check required tags", args: args{ request: newRequest(resourcesNamespace, resourcesName), loadFunc: genericDatadogMonitor, @@ -117,7 +117,6 @@ func TestReconcileDatadogMonitor_Reconcile(t *testing.T) { return err } assert.Contains(t, dm.Spec.Tags, "generated:kubernetes") - assert.False(t, dm.Status.Primary) return nil }, }, diff --git a/internal/controller/datadogmonitor/finalizer.go b/internal/controller/datadogmonitor/finalizer.go index 2fd4081b4d..3e60a39d63 100644 --- a/internal/controller/datadogmonitor/finalizer.go +++ b/internal/controller/datadogmonitor/finalizer.go @@ -10,10 +10,10 @@ import ( "fmt" "github.com/go-logr/logr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/client" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" ) @@ -21,68 +21,28 @@ const ( datadogMonitorFinalizer = "finalizer.monitor.datadoghq.com" ) -func (r *Reconciler) handleFinalizer(logger logr.Logger, dm *datadoghqv1alpha1.DatadogMonitor) (ctrl.Result, error) { - // Check if the DatadogMonitor instance is marked to be deleted, which is indicated by the deletion timestamp being set. - if dm.GetDeletionTimestamp() != nil { - if controllerutil.ContainsFinalizer(dm, datadogMonitorFinalizer) { - if err := r.finalizeDatadogMonitor(logger, dm); err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err - } - - controllerutil.RemoveFinalizer(dm, datadogMonitorFinalizer) - err := r.client.Update(context.TODO(), dm) - if err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err - } +func (r *Reconciler) deleteResource(logger logr.Logger) finalizer.ResourceDeleteFunc { + return func(ctx context.Context, k8sObj client.Object, datadogID string) error { + dm, ok := k8sObj.(*datadoghqv1alpha1.DatadogMonitor) + if !ok { + return fmt.Errorf("unexpected object type %T", k8sObj) } - // Requeue until the object was properly deleted by Kuberentes - return ctrl.Result{RequeueAfter: defaultRequeuePeriod}, nil - } - - // Add finalizer for this resource if it doesn't already exist. - if !controllerutil.ContainsFinalizer(dm, datadogMonitorFinalizer) { - if err := r.addFinalizer(logger, dm); err != nil { - return ctrl.Result{RequeueAfter: defaultErrRequeuePeriod}, err + if r.forwarders != nil { + r.forwarders.Unregister(dm) } - return ctrl.Result{RequeueAfter: defaultRequeuePeriod}, nil - } - - // Proceed in reconcile loop. - return ctrl.Result{}, nil -} - -func (r *Reconciler) finalizeDatadogMonitor(logger logr.Logger, dm *datadoghqv1alpha1.DatadogMonitor) error { - if r.forwarders != nil { - r.forwarders.Unregister(dm) - } - - if dm.Status.Primary { - err := deleteMonitor(r.datadogAuth, r.datadogClient, dm.Status.ID) - if err != nil { - logger.Error(err, "failed to finalize monitor", "Monitor ID", fmt.Sprint(dm.Status.ID)) - - return err + if dm.Status.Primary { + err := deleteMonitor(r.datadogAuth, r.datadogClient, dm.Status.ID) + if err != nil { + logger.Error(err, "failed to finalize monitor", "Monitor ID", fmt.Sprint(dm.Status.ID)) + return err + } + logger.Info("Successfully finalized DatadogMonitor", "Monitor ID", fmt.Sprint(dm.Status.ID)) + event := buildEventInfo(dm.Name, dm.Namespace, datadog.DeletionEvent) + r.recordEvent(dm, event) } - logger.Info("Successfully finalized DatadogMonitor", "Monitor ID", fmt.Sprint(dm.Status.ID)) - event := buildEventInfo(dm.Name, dm.Namespace, datadog.DeletionEvent) - r.recordEvent(dm, event) - } - return nil -} - -func (r *Reconciler) addFinalizer(logger logr.Logger, dm *datadoghqv1alpha1.DatadogMonitor) error { - logger.Info("Adding Finalizer for the DatadogMonitor") - - controllerutil.AddFinalizer(dm, datadogMonitorFinalizer) - - err := r.client.Update(context.TODO(), dm) - if err != nil { - logger.Error(err, "failed to update DatadogMonitor with finalizer", "Monitor ID", fmt.Sprint(dm.Status.ID)) - return err + return nil } - - return nil } diff --git a/internal/controller/datadogmonitor/finalizer_test.go b/internal/controller/datadogmonitor/finalizer_test.go index 1471532d47..cd20c55546 100644 --- a/internal/controller/datadogmonitor/finalizer_test.go +++ b/internal/controller/datadogmonitor/finalizer_test.go @@ -7,6 +7,7 @@ package datadogmonitor import ( "context" + "fmt" "testing" "time" @@ -20,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/internal/controller/finalizer" ) var ( @@ -90,7 +92,8 @@ func Test_handleFinalizer(t *testing.T) { testMonitor := &datadoghqv1alpha1.DatadogMonitor{} _ = r.client.Get(context.TODO(), client.ObjectKey{Namespace: "foo", Name: test.objectName}, testMonitor) - _, err := r.handleFinalizer(reqLogger, testMonitor) + final := finalizer.NewFinalizer(reqLogger, r.client, r.deleteResource(reqLogger), defaultErrRequeuePeriod) + _, err := final.HandleFinalizer(context.TODO(), testMonitor, fmt.Sprint(testMonitor.Status.ID), datadogMonitorFinalizer) assert.NoError(t, err) if test.finalizerShouldExist { diff --git a/internal/controller/datadogslo/controller.go b/internal/controller/datadogslo/controller.go index 1a664d1157..4017f3176d 100644 --- a/internal/controller/datadogslo/controller.go +++ b/internal/controller/datadogslo/controller.go @@ -119,8 +119,7 @@ func (r *Reconciler) internalReconcile(ctx context.Context, req reconcile.Reques final := finalizer.NewFinalizer( logger, r.client, - r.deleteResource(logger, instance), - defaultRequeuePeriod, + r.deleteResource(logger), defaultErrRequeuePeriod, ) if result, err = final.HandleFinalizer(ctx, instance, instance.Status.ID, datadogSLOFinalizer); ctrutils.ShouldReturn(result, err) { @@ -317,7 +316,7 @@ func (r *Reconciler) update(logger logr.Logger, instance *v1alpha1.DatadogSLO, s return nil } -func (r *Reconciler) deleteResource(logger logr.Logger, instance *v1alpha1.DatadogSLO) finalizer.ResourceDeleteFunc { +func (r *Reconciler) deleteResource(logger logr.Logger) finalizer.ResourceDeleteFunc { return func(ctx context.Context, k8sObj client.Object, datadogID string) error { if datadogID != "" { kind := k8sObj.GetObjectKind().GroupVersionKind().Kind @@ -330,7 +329,7 @@ func (r *Reconciler) deleteResource(logger logr.Logger, instance *v1alpha1.Datad logger.Info("Successfully deleted object", "kind", kind, "ID", datadogID) } } - r.recordEvent(instance, buildEventInfo(k8sObj.GetName(), k8sObj.GetNamespace(), datadog.DeletionEvent)) + r.recordEvent(k8sObj, buildEventInfo(k8sObj.GetName(), k8sObj.GetNamespace(), datadog.DeletionEvent)) return nil } } diff --git a/internal/controller/datadogslo/controller_test.go b/internal/controller/datadogslo/controller_test.go index e0bd91fc0d..962d410d27 100644 --- a/internal/controller/datadogslo/controller_test.go +++ b/internal/controller/datadogslo/controller_test.go @@ -53,6 +53,7 @@ func TestReconciler_Reconcile(t *testing.T) { name string request ctrl.Request expectedResult ctrl.Result + reconcileCount int // number of reconcile loops to run; defaults to 1 mockOn func(t *testing.T, m *mockedFields) datadogClientHandler http.HandlerFunc }{ @@ -64,6 +65,7 @@ func TestReconciler_Reconcile(t *testing.T) { Name: resourceName, }, }, + reconcileCount: 2, mockOn: func(t *testing.T, m *mockedFields) { _ = m.k8sClient.Create(context.TODO(), defaultSLO()) }, @@ -93,6 +95,7 @@ func TestReconciler_Reconcile(t *testing.T) { Name: resourceName, }, }, + reconcileCount: 2, mockOn: func(t *testing.T, m *mockedFields) { _ = m.k8sClient.Create(context.TODO(), defaultSLO()) }, @@ -109,6 +112,7 @@ func TestReconciler_Reconcile(t *testing.T) { Name: resourceName, }, }, + reconcileCount: 2, mockOn: func(t *testing.T, m *mockedFields) { slo := defaultSLO() slo.Status.ID = "SLO123" @@ -149,7 +153,14 @@ func TestReconciler_Reconcile(t *testing.T) { log: testLogger, } - res, _ := r.Reconcile(ctx, tt.request) + reconcileCount := tt.reconcileCount + if reconcileCount == 0 { + reconcileCount = 1 + } + var res ctrl.Result + for i := 0; i < reconcileCount; i++ { + res, _ = r.Reconcile(ctx, tt.request) + } assert.Equal(t, tt.expectedResult, res) }) } diff --git a/internal/controller/finalizer/finalizer.go b/internal/controller/finalizer/finalizer.go index fe495dbb52..0c418040ea 100644 --- a/internal/controller/finalizer/finalizer.go +++ b/internal/controller/finalizer/finalizer.go @@ -15,6 +15,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +// ResourceDeleteFunc performs controller-specific cleanup when a resource is being deleted. +// If it returns an error, the finalizer is NOT removed — the object stays in a terminating +// state and the controller retries on the next reconcile. If it returns nil, the finalizer +// is removed and Kubernetes proceeds with deletion. type ResourceDeleteFunc func(ctx context.Context, k8sObj client.Object, datadogID string) error type Finalizer struct { @@ -22,7 +26,6 @@ type Finalizer struct { client client.Client deleteFunc ResourceDeleteFunc - defaultRequeuePeriod time.Duration defaultErrRequeuePeriod time.Duration } @@ -30,14 +33,12 @@ func NewFinalizer( logger logr.Logger, client client.Client, deleteFunc ResourceDeleteFunc, - defaultRequeuePeriod time.Duration, defaultErrRequeuePeriod time.Duration, ) *Finalizer { return &Finalizer{ logger: logger, client: client, deleteFunc: deleteFunc, - defaultRequeuePeriod: defaultRequeuePeriod, defaultErrRequeuePeriod: defaultErrRequeuePeriod, } } @@ -58,6 +59,9 @@ func (f *Finalizer) HandleFinalizer(ctx context.Context, clientObj client.Object if err != nil { return ctrl.Result{Requeue: true, RequeueAfter: f.defaultErrRequeuePeriod}, err } + // Requeue immediately so the next reconcile works with the + // updated object from the API server. + return ctrl.Result{Requeue: true}, nil } } else { f.logger.Info("Object being deleted", "kind", clientObj.GetObjectKind(), "finalizername", finalizerName) @@ -74,6 +78,8 @@ func (f *Finalizer) HandleFinalizer(ctx context.Context, clientObj client.Object return ctrl.Result{Requeue: true, RequeueAfter: f.defaultErrRequeuePeriod}, err } } + // Requeue while the object is still being deleted. + return ctrl.Result{Requeue: true}, nil } return ctrl.Result{}, nil } diff --git a/internal/controller/finalizer/finalizer_test.go b/internal/controller/finalizer/finalizer_test.go index 59df7504f8..f6bdc4c3b8 100644 --- a/internal/controller/finalizer/finalizer_test.go +++ b/internal/controller/finalizer/finalizer_test.go @@ -7,6 +7,10 @@ package finalizer import ( "context" + "fmt" + "testing" + "time" + datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,8 +21,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "testing" - "time" ) type testResource struct { @@ -36,6 +38,14 @@ func Test_HandleFinalizer(t *testing.T) { s.AddKnownTypes(datadoghqv1alpha1.GroupVersion, &testResource{}) finalizerName := "test_resource.finalizer" metaNow := metav1.NewTime(time.Now()) + errRequeuePeriod := time.Minute + + noopDelete := func(ctx context.Context, k8sObj client.Object, datadogID string) error { + return nil + } + failDelete := func(ctx context.Context, k8sObj client.Object, datadogID string) error { + return fmt.Errorf("API error") + } tests := []struct { name string @@ -46,7 +56,7 @@ func Test_HandleFinalizer(t *testing.T) { deleterFunc ResourceDeleteFunc }{ { - name: "check if object deletion timestamp is empty add finalizer if not exists", + name: "not deleting, no finalizer: adds finalizer and requeues immediately", clientObject: testResource{ TypeMeta: metav1.TypeMeta{ Kind: "TestResource", @@ -56,13 +66,26 @@ func Test_HandleFinalizer(t *testing.T) { }, }, finalizerShouldExists: true, - expectedResult: ctrl.Result{}, - deleterFunc: func(ctx context.Context, k8sObj client.Object, datadogID string) error { - return nil + expectedResult: ctrl.Result{Requeue: true}, + deleterFunc: noopDelete, + }, + { + name: "not deleting, already has finalizer: no-op, proceed to reconciliation", + clientObject: testResource{ + TypeMeta: metav1.TypeMeta{ + Kind: "TestResource", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test resource", + Finalizers: []string{finalizerName}, + }, }, + finalizerShouldExists: true, + expectedResult: ctrl.Result{}, + deleterFunc: noopDelete, }, { - name: "remove finalizer when deletion timestamp is set and finalizer is exits", + name: "deleting, has finalizer, deleteFunc succeeds: removes finalizer, requeues immediately", clientObject: testResource{ TypeMeta: metav1.TypeMeta{ Kind: "TestResource", @@ -74,28 +97,49 @@ func Test_HandleFinalizer(t *testing.T) { }, }, finalizerShouldExists: false, - expectedResult: ctrl.Result{}, - deleterFunc: func(ctx context.Context, k8sObj client.Object, datadogID string) error { - return nil + expectedResult: ctrl.Result{Requeue: true}, + deleterFunc: noopDelete, + }, + { + name: "deleting, has finalizer, deleteFunc fails: keeps finalizer, requeues with error period", + clientObject: testResource{ + TypeMeta: metav1.TypeMeta{ + Kind: "TestResource", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test resource", + DeletionTimestamp: &metaNow, + Finalizers: []string{finalizerName}, + }, }, + finalizerShouldExists: true, + expectedResult: ctrl.Result{Requeue: true, RequeueAfter: errRequeuePeriod}, + expectedErr: true, + deleterFunc: failDelete, }, + // Note: "deleting, no finalizer" is not testable because the fake client + // (and the real API server) reject objects with a deletionTimestamp but no + // finalizers — Kubernetes would have already garbage-collected such objects. } for _, tt := range tests { - // arrange - fakeClient := fake.NewClientBuilder().WithObjects(&tt.clientObject).Build() - finalizer := NewFinalizer(testLogger, fakeClient, tt.deleterFunc, time.Minute, time.Minute) + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithObjects(&tt.clientObject).Build() + finalizer := NewFinalizer(testLogger, fakeClient, tt.deleterFunc, errRequeuePeriod) - // act - res, err := finalizer.HandleFinalizer(context.TODO(), &tt.clientObject, "123", finalizerName) + res, err := finalizer.HandleFinalizer(context.TODO(), &tt.clientObject, "123", finalizerName) - // assert - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, res) - if tt.finalizerShouldExists { - assert.True(t, controllerutil.ContainsFinalizer(&tt.clientObject, finalizerName)) - } else { - assert.False(t, controllerutil.ContainsFinalizer(&tt.clientObject, finalizerName)) - } + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectedResult, res) + if tt.finalizerShouldExists { + assert.True(t, controllerutil.ContainsFinalizer(&tt.clientObject, finalizerName)) + } else { + assert.False(t, controllerutil.ContainsFinalizer(&tt.clientObject, finalizerName)) + } + }) } }