diff --git a/internal/controllers/reconciliation/controller.go b/internal/controllers/reconciliation/controller.go index 08f7d135..e940e2fa 100644 --- a/internal/controllers/reconciliation/controller.go +++ b/internal/controllers/reconciliation/controller.go @@ -213,6 +213,16 @@ func (c *Controller) reconcileSnapshot(ctx context.Context, comp *apiv1.Composit return false, nil // already deleted - nothing to do } + if res.ForceDeletion && len(current.GetFinalizers()) > 0 { + current.SetFinalizers([]string{}) + err := c.upstreamClient.Update(ctx, current, client.FieldOwner("eno")) + if err != nil { + return true, fmt.Errorf("removing finalizers: %w", err) + } + logger.V(0).Info("removed finalizers before deletion") + return true, nil + } + reconciliationActions.WithLabelValues("delete").Inc() err := c.upstreamClient.Delete(ctx, current) if err != nil { diff --git a/internal/controllers/reconciliation/crud_test.go b/internal/controllers/reconciliation/crud_test.go index 0f433708..fdc3e3fc 100644 --- a/internal/controllers/reconciliation/crud_test.go +++ b/internal/controllers/reconciliation/crud_test.go @@ -1072,3 +1072,37 @@ func TestResourceSelector(t *testing.T) { }) assert.True(t, errors.IsNotFound(mgr.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test-obj-1"}, &corev1.ConfigMap{}))) } + +func TestForceDeletion(t *testing.T) { + ctx := testutil.NewContext(t) + mgr := testutil.NewManager(t) + upstream := mgr.GetClient() + + testutil.WithFakeExecutor(t, mgr, func(ctx context.Context, s *apiv1.Synthesizer, input *krmv1.ResourceList) (*krmv1.ResourceList, error) { + output := &krmv1.ResourceList{} + output.Items = []*unstructured.Unstructured{{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "test-obj-1", + "namespace": "default", + "annotations": map[string]any{"eno.azure.io/force-deletion": "true"}, + "finalizers": []any{"eno.azure.io/test", "eno.azure.io/another-one"}, + }, + }, + }} + return output, nil + }) + + registerControllers(t, mgr) + setupTestSubject(t, mgr) + mgr.Start(t) + _, comp := writeGenericComposition(t, upstream) + waitForReadiness(t, mgr, comp, nil, nil) + + require.NoError(t, upstream.Delete(ctx, comp)) + testutil.Eventually(t, func() bool { + return errors.IsNotFound(upstream.Get(ctx, client.ObjectKeyFromObject(comp), comp)) + }) +} diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 80b36260..4e61db23 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -284,6 +284,9 @@ func (r *Resource) SnapshotWithOverrides(ctx context.Context, comp *apiv1.Compos const replaceKey = "eno.azure.io/replace" snap.Replace = cascadeAnnotation(comp, copy, replaceKey) == "true" + const forceDeletionKey = "eno.azure.io/force-deletion" + snap.ForceDeletion = cascadeAnnotation(comp, copy, forceDeletionKey) == "true" + const deletionStratKey = "eno.azure.io/deletion-strategy" snap.Orphan = strings.EqualFold(cascadeAnnotation(comp, copy, deletionStratKey), "orphan") snap.Orphan = !r.isPatch && strings.EqualFold(cascadeAnnotation(comp, copy, deletionStratKey), "orphan") @@ -314,6 +317,7 @@ type Snapshot struct { Disable bool DisableUpdates bool Replace bool + ForceDeletion bool Orphan bool ForegroundDeletion bool diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go index 3c8d8f8f..c8f9675c 100644 --- a/internal/resource/resource_test.go +++ b/internal/resource/resource_test.go @@ -43,6 +43,7 @@ var newResourceTests = []struct { "eno.azure.io/disable-reconciliation": "true", "eno.azure.io/disable-updates": "true", "eno.azure.io/deletion-strategy": "orphan", + "eno.azure.io/force-deletion": "true", "eno.azure.io/deletion-group": "0", "eno.azure.io/fail-open": "true", "eno.azure.io/overrides": "[{\"path\":\".self.foo\"}, {\"path\":\".self.bar\", \"condition\": \"false\"}]" @@ -64,6 +65,7 @@ var newResourceTests = []struct { assert.True(t, r.Replace) assert.True(t, r.Orphan) assert.False(t, r.ForegroundDeletion) + assert.True(t, r.ForceDeletion) assert.True(t, *r.FailOpen) assert.Equal(t, 0, *r.deletionGroup) assert.Equal(t, int(250), r.readinessGroup)