This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 84684e3f fix: stop requeueing ApisixConsumer forever after a 
successful delete (#2850)
84684e3f is described below

commit 84684e3fbc08e2194202b26fb89d27d6f7311ea3
Author: Ali Asghar <[email protected]>
AuthorDate: Tue Sep 1 00:48:06 2026 -0700

    fix: stop requeueing ApisixConsumer forever after a successful delete 
(#2850)
---
 internal/controller/apisixconsumer_controller.go   |   2 +
 .../controller/apisixconsumer_controller_test.go   | 139 +++++++++++++++++++++
 2 files changed, 141 insertions(+)

diff --git a/internal/controller/apisixconsumer_controller.go 
b/internal/controller/apisixconsumer_controller.go
index 95c24075..2652be7f 100644
--- a/internal/controller/apisixconsumer_controller.go
+++ b/internal/controller/apisixconsumer_controller.go
@@ -73,6 +73,8 @@ func (r *ApisixConsumerReconciler) Reconcile(ctx 
context.Context, req ctrl.Reque
                                r.Log.Error(err, "failed to delete provider", 
"ApisixConsumer", utils.NamespacedName(ac))
                                return ctrl.Result{}, err
                        }
+                       r.Log.Info("deleted apisix consumer", "ApisixConsumer", 
utils.NamespacedName(ac))
+                       return ctrl.Result{}, nil
                }
                r.Log.Error(err, "failed to get ApisixConsumer", "request", 
req.NamespacedName)
                return ctrl.Result{}, err
diff --git a/internal/controller/apisixconsumer_controller_test.go 
b/internal/controller/apisixconsumer_controller_test.go
new file mode 100644
index 00000000..7d3bc49c
--- /dev/null
+++ b/internal/controller/apisixconsumer_controller_test.go
@@ -0,0 +1,139 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 controller
+
+import (
+       "context"
+       "errors"
+       "net/http"
+       "testing"
+
+       "github.com/go-logr/logr"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       k8serrors "k8s.io/apimachinery/pkg/api/errors"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/types"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
+       "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+const testConsumerNamespace = "default"
+
+// recordingProvider records the objects passed to Delete and can be told to 
fail.
+type recordingProvider struct {
+       deleted   []types.NamespacedName
+       deleteErr error
+}
+
+func (p *recordingProvider) Register(string, *http.ServeMux) {}
+
+func (p *recordingProvider) Update(context.Context, 
*provider.TranslateContext, client.Object) error {
+       return nil
+}
+
+func (p *recordingProvider) Delete(_ context.Context, obj client.Object) error 
{
+       p.deleted = append(p.deleted, types.NamespacedName{Namespace: 
obj.GetNamespace(), Name: obj.GetName()})
+       return p.deleteErr
+}
+
+func (p *recordingProvider) Start(context.Context) error { return nil }
+
+func (p *recordingProvider) NeedLeaderElection() bool { return true }
+
+func newApisixConsumerReconciler(t *testing.T, cli client.Client, p 
provider.Provider) *ApisixConsumerReconciler {
+       t.Helper()
+
+       // A readiness manager with no registered GVKs becomes ready as soon as 
it is
+       // started. Readier.Done blocks on the started channel, so it must be 
started.
+       readier := readiness.NewReadinessManager(cli, logr.Discard())
+       require.NoError(t, readier.Start(context.Background()))
+
+       return &ApisixConsumerReconciler{
+               Client:   cli,
+               Scheme:   cli.Scheme(),
+               Log:      logr.Discard(),
+               Provider: p,
+               Readier:  readier,
+       }
+}
+
+func apisixConsumerScheme(t *testing.T) *runtime.Scheme {
+       t.Helper()
+       scheme := runtime.NewScheme()
+       require.NoError(t, apiv2.AddToScheme(scheme))
+       return scheme
+}
+
+// A deleted ApisixConsumer must be removed from the provider and then reported
+// as reconciled. Returning the NotFound error instead makes controller-runtime
+// treat the reconcile as failed and requeue it indefinitely with exponential
+// backoff, even though the delete succeeded.
+func TestApisixConsumerReconcile_DeletedObjectDoesNotRequeue(t *testing.T) {
+       cli := 
fake.NewClientBuilder().WithScheme(apisixConsumerScheme(t)).Build()
+       p := &recordingProvider{}
+       r := newApisixConsumerReconciler(t, cli, p)
+
+       key := types.NamespacedName{Namespace: testConsumerNamespace, Name: 
"gone"}
+       result, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: key})
+
+       require.NoError(t, err)
+       assert.Equal(t, ctrl.Result{}, result)
+       assert.Equal(t, []types.NamespacedName{key}, p.deleted)
+}
+
+// A provider failure must still surface, so the fix does not swallow real 
errors.
+func TestApisixConsumerReconcile_DeleteErrorIsReturned(t *testing.T) {
+       cli := 
fake.NewClientBuilder().WithScheme(apisixConsumerScheme(t)).Build()
+       p := &recordingProvider{deleteErr: errors.New("provider unavailable")}
+       r := newApisixConsumerReconciler(t, cli, p)
+
+       key := types.NamespacedName{Namespace: testConsumerNamespace, Name: 
"gone"}
+       _, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: key})
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "provider unavailable")
+}
+
+// A Get failure that is not NotFound must be returned, and must not be 
mistaken
+// for a deletion.
+func TestApisixConsumerReconcile_NonNotFoundGetErrorIsReturned(t *testing.T) {
+       cli := fake.NewClientBuilder().
+               WithScheme(apisixConsumerScheme(t)).
+               WithInterceptorFuncs(interceptor.Funcs{
+                       Get: func(context.Context, client.WithWatch, 
client.ObjectKey, client.Object, ...client.GetOption) error {
+                               return 
k8serrors.NewInternalError(errors.New("boom"))
+                       },
+               }).
+               Build()
+       p := &recordingProvider{}
+       r := newApisixConsumerReconciler(t, cli, p)
+
+       key := types.NamespacedName{Namespace: testConsumerNamespace, Name: 
"present"}
+       _, err := r.Reconcile(context.Background(), 
ctrl.Request{NamespacedName: key})
+
+       require.Error(t, err)
+       assert.True(t, k8serrors.IsInternalError(err), "want an internal error, 
got %v", err)
+       assert.Empty(t, p.deleted, "a non-NotFound Get error must not trigger a 
provider delete")
+}

Reply via email to