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 6b95e2aa fix: surface Gateway API route translation failures in status 
(#2840) (#2898)
6b95e2aa is described below

commit 6b95e2aa9d598ec09814cb982d18880c94c98e3b
Author: 玉升 <[email protected]>
AuthorDate: Mon Oct 12 15:32:09 2026 +0800

    fix: surface Gateway API route translation failures in status (#2840) 
(#2898)
    
    Signed-off-by: yusheng.wang <[email protected]>
---
 .../controller/apisixconsumer_controller_test.go   |  5 +-
 internal/controller/grpcroute_controller.go        | 17 ++--
 ...etract_test.go => grpcroute_controller_test.go} | 90 ++++++----------------
 internal/controller/httproute_controller.go        | 26 ++++---
 .../httproute_controller_retract_test.go           | 39 +++++++++-
 internal/controller/tcproute_controller.go         | 16 ++--
 internal/controller/tlsroute_controller.go         | 16 ++--
 internal/controller/udproute_controller.go         | 16 ++--
 8 files changed, 126 insertions(+), 99 deletions(-)

diff --git a/internal/controller/apisixconsumer_controller_test.go 
b/internal/controller/apisixconsumer_controller_test.go
index 12cde685..d85866cc 100644
--- a/internal/controller/apisixconsumer_controller_test.go
+++ b/internal/controller/apisixconsumer_controller_test.go
@@ -46,18 +46,19 @@ import (
 const testConsumerNamespace = "default"
 
 // recordingProvider records the objects passed to Update and Delete, and can 
be
-// told to fail a delete. Shared by the reconciler tests in this package.
+// told to fail an update or a delete. Shared by the reconciler tests in this 
package.
 type recordingProvider struct {
        updated   int
        deleted   []types.NamespacedName
        deleteErr error
+       updateErr error
 }
 
 func (p *recordingProvider) Register(string, *http.ServeMux) {}
 
 func (p *recordingProvider) Update(context.Context, 
*provider.TranslateContext, client.Object) error {
        p.updated++
-       return nil
+       return p.updateErr
 }
 
 func (p *recordingProvider) Delete(_ context.Context, obj client.Object) error 
{
diff --git a/internal/controller/grpcroute_controller.go 
b/internal/controller/grpcroute_controller.go
index 5129af89..1218afcb 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -256,6 +256,17 @@ func (r *GRPCRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
 
        ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
 
+       // Translate and sync before writing Accepted=True. A translation or
+       // buildConfig failure from Provider.Update used to leave a green status
+       // and only requeue, matching neither Ingress nor ApisixGlobalRule.
+       var syncErr error
+       if isRouteAccepted(gateways) && err == nil {
+               if syncErr = r.Provider.Update(ctx, tctx, gr); syncErr != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = syncErr.Error()
+               }
+       }
+
        // TODO: diff the old and new status
        gr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
        for _, gateway := range gateways {
@@ -287,11 +298,7 @@ func (r *GRPCRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
        UpdateStatus(r.Updater, r.Log, tctx)
 
        if isRouteAccepted(gateways) && err == nil {
-               routeToUpdate := gr
-               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
-                       return ctrl.Result{}, err
-               }
-               return ctrl.Result{}, nil
+               return ctrl.Result{}, syncErr
        }
 
        // The route still resolves to one of our Gateways but no parent 
accepts it any
diff --git a/internal/controller/httproute_controller_retract_test.go 
b/internal/controller/grpcroute_controller_test.go
similarity index 54%
copy from internal/controller/httproute_controller_retract_test.go
copy to internal/controller/grpcroute_controller_test.go
index 857c2fa5..35e7e869 100644
--- a/internal/controller/httproute_controller_retract_test.go
+++ b/internal/controller/grpcroute_controller_test.go
@@ -40,20 +40,7 @@ import (
        "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
 )
 
-const (
-       retractGatewayNamespace = "infra"
-       retractRouteNamespace   = "tenant"
-       retractRouteName        = "route"
-)
-
-// newHTTPRouteRetractFixture builds a Gateway of our class in 
retractGatewayNamespace
-// and an HTTPRoute in retractRouteNamespace that names it as its parent. from
-// controls the listener's allowedRoutes, which is what revoking 
cross-namespace
-// access changes.
-func newHTTPRouteRetractFixture(
-       t *testing.T,
-       from gatewayv1.FromNamespaces,
-) (*HTTPRouteReconciler, *recordingProvider, *recordingUpdater) {
+func newGRPCRouteFixture(t *testing.T) (*GRPCRouteReconciler, 
*recordingProvider, *recordingUpdater) {
        t.Helper()
 
        scheme := runtime.NewScheme()
@@ -61,6 +48,7 @@ func newHTTPRouteRetractFixture(
        require.NoError(t, gatewayv1.Install(scheme))
        require.NoError(t, v1alpha1.AddToScheme(scheme))
 
+       from := gatewayv1.NamespacesFromAll
        gatewayClass := &gatewayv1.GatewayClass{
                ObjectMeta: metav1.ObjectMeta{Name: "apisix"},
                Spec: gatewayv1.GatewayClassSpec{
@@ -81,13 +69,9 @@ func newHTTPRouteRetractFixture(
                        }},
                },
        }
-       route := &gatewayv1.HTTPRoute{
+       route := &gatewayv1.GRPCRoute{
                ObjectMeta: metav1.ObjectMeta{Namespace: retractRouteNamespace, 
Name: retractRouteName},
-               Spec: gatewayv1.HTTPRouteSpec{
-                       // A route with hostnames of its own is what makes 
filterHostnames fail
-                       // once no listener matches, which is how a route-wide 
reason used to
-                       // overwrite the parent's own.
-                       Hostnames: []gatewayv1.Hostname{"tenant.example"},
+               Spec: gatewayv1.GRPCRouteSpec{
                        CommonRouteSpec: gatewayv1.CommonRouteSpec{
                                ParentRefs: []gatewayv1.ParentReference{{
                                        Name:      
gatewayv1.ObjectName(gateway.Name),
@@ -107,7 +91,7 @@ func newHTTPRouteRetractFixture(
 
        prov := &recordingProvider{}
        updater := &recordingUpdater{}
-       return &HTTPRouteReconciler{
+       return &GRPCRouteReconciler{
                Client:   cli,
                Scheme:   scheme,
                Log:      logr.Discard(),
@@ -117,61 +101,33 @@ func newHTTPRouteRetractFixture(
        }, prov, updater
 }
 
-func reconcileRetractHTTPRoute(t *testing.T, r *HTTPRouteReconciler) 
(ctrl.Result, error) {
+func grpcAcceptedConditionFromUpdater(t *testing.T, updater *recordingUpdater) 
*metav1.Condition {
        t.Helper()
-       return r.Reconcile(context.Background(), ctrl.Request{
-               NamespacedName: k8stypes.NamespacedName{Namespace: 
retractRouteNamespace, Name: retractRouteName},
-       })
-}
-
-var retractRouteKey = k8stypes.NamespacedName{Namespace: 
retractRouteNamespace, Name: retractRouteName}
-
-// Narrowing a listener's allowedRoutes leaves the HTTPRoute in place but 
stops it
-// being accepted. The configuration an earlier reconcile published must be
-// retracted, otherwise the data plane keeps serving a route the Gateway no 
longer
-// admits and only deleting the HTTPRoute clears it.
-func TestHTTPRouteReconcile_RetractsWhenListenerStopsAllowingRoute(t 
*testing.T) {
-       r, prov, updater := newHTTPRouteRetractFixture(t, 
gatewayv1.NamespacesFromSame)
-
-       result, err := reconcileRetractHTTPRoute(t, r)
-
-       require.NoError(t, err)
-       assert.Equal(t, ctrl.Result{}, result)
-       assert.Equal(t, []k8stypes.NamespacedName{retractRouteKey}, 
prov.deleted)
-       assert.Zero(t, prov.updated, "a route that is not accepted must not be 
published")
-
-       // The reason must say why this parent rejected the route. The 
route-wide
-       // status derived from filterHostnames would otherwise report
-       // NoMatchingListenerHostname, which is a symptom of nothing having 
matched
-       // rather than the cause.
-       require.Len(t, updater.updates, 1)
-       mutated, ok := 
updater.updates[0].Mutator.Mutate(&gatewayv1.HTTPRoute{}).(*gatewayv1.HTTPRoute)
+       require.NotEmpty(t, updater.updates)
+       mutated, ok := 
updater.updates[0].Mutator.Mutate(&gatewayv1.GRPCRoute{}).(*gatewayv1.GRPCRoute)
        require.True(t, ok)
        require.Len(t, mutated.Status.Parents, 1)
        accepted := 
meta.FindStatusCondition(mutated.Status.Parents[0].Conditions, 
string(gatewayv1.RouteConditionAccepted))
        require.NotNil(t, accepted)
-       assert.Equal(t, metav1.ConditionFalse, accepted.Status)
-       assert.Equal(t, string(gatewayv1.RouteReasonNotAllowedByListeners), 
accepted.Reason)
+       return accepted
 }
 
-// An accepted route must still be published and must not be retracted.
-func TestHTTPRouteReconcile_PublishesAcceptedRoute(t *testing.T) {
-       r, prov, _ := newHTTPRouteRetractFixture(t, gatewayv1.NamespacesFromAll)
+// A translation / Provider.Update failure must be visible on the route status
+// instead of Accepted=True plus a silent requeue.
+func TestGRPCRouteReconcile_UpdateErrorIsVisibleOnStatus(t *testing.T) {
+       r, prov, updater := newGRPCRouteFixture(t)
+       prov.updateErr = errors.New("translation failed")
 
-       _, err := reconcileRetractHTTPRoute(t, r)
+       _, err := r.Reconcile(context.Background(), ctrl.Request{
+               NamespacedName: k8stypes.NamespacedName{Namespace: 
retractRouteNamespace, Name: retractRouteName},
+       })
 
-       require.NoError(t, err)
-       assert.Empty(t, prov.deleted, "an accepted route must not be retracted")
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "translation failed")
        assert.Equal(t, 1, prov.updated)
-}
+       assert.Empty(t, prov.deleted)
 
-// A provider failure while retracting must surface so the reconcile is 
retried.
-func TestHTTPRouteReconcile_RetractErrorIsReturned(t *testing.T) {
-       r, prov, _ := newHTTPRouteRetractFixture(t, 
gatewayv1.NamespacesFromSame)
-       prov.deleteErr = errors.New("provider unavailable")
-
-       _, err := reconcileRetractHTTPRoute(t, r)
-
-       require.Error(t, err)
-       assert.Contains(t, err.Error(), "provider unavailable")
+       accepted := grpcAcceptedConditionFromUpdater(t, updater)
+       assert.Equal(t, metav1.ConditionFalse, accepted.Status)
+       assert.Contains(t, accepted.Message, "translation failed")
 }
diff --git a/internal/controller/httproute_controller.go 
b/internal/controller/httproute_controller.go
index a2741791..5050cea9 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -254,6 +254,22 @@ func (r *HTTPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
                acceptStatus.msg = err.Error()
        }
 
+       // Translate and sync before writing Accepted=True. A translation or
+       // buildConfig failure from Provider.Update used to leave a green status
+       // and only requeue, matching neither Ingress nor ApisixGlobalRule.
+       var syncErr error
+       if isRouteAccepted(gateways) && err == nil {
+               routeToUpdate := hr
+               if filteredHTTPRoute != nil {
+                       r.Log.V(1).Info("filtered httproute", "httproute", 
utils.NamespacedName(filteredHTTPRoute))
+                       routeToUpdate = filteredHTTPRoute
+               }
+               if syncErr = r.Provider.Update(ctx, tctx, routeToUpdate); 
syncErr != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = syncErr.Error()
+               }
+       }
+
        // TODO: diff the old and new status
        hr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
        for _, gateway := range gateways {
@@ -285,15 +301,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
        UpdateStatus(r.Updater, r.Log, tctx)
 
        if isRouteAccepted(gateways) && err == nil {
-               routeToUpdate := hr
-               if filteredHTTPRoute != nil {
-                       r.Log.V(1).Info("filtered httproute", "httproute", 
utils.NamespacedName(filteredHTTPRoute))
-                       routeToUpdate = filteredHTTPRoute
-               }
-               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
-                       return ctrl.Result{}, err
-               }
-               return ctrl.Result{}, nil
+               return ctrl.Result{}, syncErr
        }
 
        // The route still resolves to one of our Gateways but no parent 
accepts it any
diff --git a/internal/controller/httproute_controller_retract_test.go 
b/internal/controller/httproute_controller_retract_test.go
index 857c2fa5..c0c857c0 100644
--- a/internal/controller/httproute_controller_retract_test.go
+++ b/internal/controller/httproute_controller_retract_test.go
@@ -37,6 +37,7 @@ import (
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
        "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
 )
 
@@ -100,6 +101,10 @@ func newHTTPRouteRetractFixture(
        cli := fake.NewClientBuilder().WithScheme(scheme).
                WithObjects([]client.Object{gatewayClass, gateway, route}...).
                WithStatusSubresource(route).
+               // The reconcile lists HTTPRoutePolicies by target, which the 
fake client
+               // only serves once the index exists. No policy is under test 
here.
+               WithIndex(&v1alpha1.HTTPRoutePolicy{}, indexer.PolicyTargetRefs,
+                       func(client.Object) []string { return nil }).
                Build()
 
        readier := readiness.NewReadinessManager(cli, logr.Discard())
@@ -156,13 +161,16 @@ func 
TestHTTPRouteReconcile_RetractsWhenListenerStopsAllowingRoute(t *testing.T)
 
 // An accepted route must still be published and must not be retracted.
 func TestHTTPRouteReconcile_PublishesAcceptedRoute(t *testing.T) {
-       r, prov, _ := newHTTPRouteRetractFixture(t, gatewayv1.NamespacesFromAll)
+       r, prov, updater := newHTTPRouteRetractFixture(t, 
gatewayv1.NamespacesFromAll)
 
        _, err := reconcileRetractHTTPRoute(t, r)
 
        require.NoError(t, err)
        assert.Empty(t, prov.deleted, "an accepted route must not be retracted")
        assert.Equal(t, 1, prov.updated)
+
+       accepted := acceptedConditionFromUpdater(t, updater)
+       assert.Equal(t, metav1.ConditionTrue, accepted.Status)
 }
 
 // A provider failure while retracting must surface so the reconcile is 
retried.
@@ -175,3 +183,32 @@ func TestHTTPRouteReconcile_RetractErrorIsReturned(t 
*testing.T) {
        require.Error(t, err)
        assert.Contains(t, err.Error(), "provider unavailable")
 }
+
+func acceptedConditionFromUpdater(t *testing.T, updater *recordingUpdater) 
*metav1.Condition {
+       t.Helper()
+       require.NotEmpty(t, updater.updates)
+       mutated, ok := 
updater.updates[0].Mutator.Mutate(&gatewayv1.HTTPRoute{}).(*gatewayv1.HTTPRoute)
+       require.True(t, ok)
+       require.Len(t, mutated.Status.Parents, 1)
+       accepted := 
meta.FindStatusCondition(mutated.Status.Parents[0].Conditions, 
string(gatewayv1.RouteConditionAccepted))
+       require.NotNil(t, accepted)
+       return accepted
+}
+
+// A translation / Provider.Update failure must be visible on the route status
+// instead of Accepted=True plus a silent requeue.
+func TestHTTPRouteReconcile_UpdateErrorIsVisibleOnStatus(t *testing.T) {
+       r, prov, updater := newHTTPRouteRetractFixture(t, 
gatewayv1.NamespacesFromAll)
+       prov.updateErr = errors.New("translation failed")
+
+       _, err := reconcileRetractHTTPRoute(t, r)
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "translation failed")
+       assert.Equal(t, 1, prov.updated)
+       assert.Empty(t, prov.deleted)
+
+       accepted := acceptedConditionFromUpdater(t, updater)
+       assert.Equal(t, metav1.ConditionFalse, accepted.Status)
+       assert.Contains(t, accepted.Message, "translation failed")
+}
diff --git a/internal/controller/tcproute_controller.go 
b/internal/controller/tcproute_controller.go
index b8d6f97e..8428a55e 100644
--- a/internal/controller/tcproute_controller.go
+++ b/internal/controller/tcproute_controller.go
@@ -348,6 +348,16 @@ func (r *TCPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        if r.supportsL4RoutePolicy {
                ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, 
tr.Name, KindTCPRoute)
        }
+       // Translate and sync before writing Accepted=True. A translation or
+       // buildConfig failure from Provider.Update used to leave a green status
+       // and only requeue, matching neither Ingress nor ApisixGlobalRule.
+       var syncErr error
+       if isRouteAccepted(gateways) {
+               if syncErr = r.Provider.Update(ctx, tctx, tr); syncErr != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = syncErr.Error()
+               }
+       }
        tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
        for _, gateway := range gateways {
                parentStatus := gatewayv1.RouteParentStatus{}
@@ -377,11 +387,7 @@ func (r *TCPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        })
        UpdateStatus(r.Updater, r.Log, tctx)
        if isRouteAccepted(gateways) {
-               routeToUpdate := tr
-               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
-                       return ctrl.Result{}, err
-               }
-               return ctrl.Result{}, nil
+               return ctrl.Result{}, syncErr
        }
 
        // The route still resolves to one of our Gateways but no parent 
accepts it any
diff --git a/internal/controller/tlsroute_controller.go 
b/internal/controller/tlsroute_controller.go
index 779b7603..f1b1792f 100644
--- a/internal/controller/tlsroute_controller.go
+++ b/internal/controller/tlsroute_controller.go
@@ -348,6 +348,16 @@ func (r *TLSRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        if r.supportsL4RoutePolicy {
                ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, 
tr.Name, types.KindTLSRoute)
        }
+       // Translate and sync before writing Accepted=True. A translation or
+       // buildConfig failure from Provider.Update used to leave a green status
+       // and only requeue, matching neither Ingress nor ApisixGlobalRule.
+       var syncErr error
+       if isRouteAccepted(gateways) {
+               if syncErr = r.Provider.Update(ctx, tctx, tr); syncErr != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = syncErr.Error()
+               }
+       }
        tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
        for _, gateway := range gateways {
                parentStatus := gatewayv1.RouteParentStatus{}
@@ -377,11 +387,7 @@ func (r *TLSRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        })
        UpdateStatus(r.Updater, r.Log, tctx)
        if isRouteAccepted(gateways) {
-               routeToUpdate := tr
-               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
-                       return ctrl.Result{}, err
-               }
-               return ctrl.Result{}, nil
+               return ctrl.Result{}, syncErr
        }
 
        // The route still resolves to one of our Gateways but no parent 
accepts it any
diff --git a/internal/controller/udproute_controller.go 
b/internal/controller/udproute_controller.go
index 3ca88907..a9475941 100644
--- a/internal/controller/udproute_controller.go
+++ b/internal/controller/udproute_controller.go
@@ -348,6 +348,16 @@ func (r *UDPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        if r.supportsL4RoutePolicy {
                ProcessL4RoutePolicy(r.Client, r.Log, tctx, tr.Namespace, 
tr.Name, KindUDPRoute)
        }
+       // Translate and sync before writing Accepted=True. A translation or
+       // buildConfig failure from Provider.Update used to leave a green status
+       // and only requeue, matching neither Ingress nor ApisixGlobalRule.
+       var syncErr error
+       if isRouteAccepted(gateways) {
+               if syncErr = r.Provider.Update(ctx, tctx, tr); syncErr != nil {
+                       acceptStatus.status = false
+                       acceptStatus.msg = syncErr.Error()
+               }
+       }
        tr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, 
len(gateways))
        for _, gateway := range gateways {
                parentStatus := gatewayv1.RouteParentStatus{}
@@ -377,11 +387,7 @@ func (r *UDPRouteReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (c
        })
        UpdateStatus(r.Updater, r.Log, tctx)
        if isRouteAccepted(gateways) {
-               routeToUpdate := tr
-               if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != 
nil {
-                       return ctrl.Result{}, err
-               }
-               return ctrl.Result{}, nil
+               return ctrl.Result{}, syncErr
        }
 
        // The route still resolves to one of our Gateways but no parent 
accepts it any

Reply via email to