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

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

commit 72705794b73c15fa27e829f7de241f04c92faf22
Author: Ashing Zheng <[email protected]>
AuthorDate: Thu Oct 23 15:08:29 2025 +0800

    feat: support kubernetes.io/ingress.class annotations
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/controller/indexer/indexer.go    |  5 +-
 internal/controller/ingress_controller.go | 11 +++--
 internal/controller/utils.go              |  3 +-
 internal/types/k8s.go                     | 15 +++++-
 internal/types/k8s_test.go                | 82 +++++++++++++++++++++++++++++++
 test/e2e/ingress/ingress.go               | 62 +++++++++++++++++++++++
 6 files changed, 168 insertions(+), 10 deletions(-)

diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index 8c3af2b5..f517e11d 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -451,10 +451,11 @@ func IngressClassIndexFunc(rawObj client.Object) []string 
{
 
 func IngressClassRefIndexFunc(rawObj client.Object) []string {
        ingress := rawObj.(*networkingv1.Ingress)
-       if ingress.Spec.IngressClassName == nil {
+       ingressClassName := internaltypes.GetEffectiveIngressClassName(ingress)
+       if ingressClassName == "" {
                return nil
        }
-       return []string{*ingress.Spec.IngressClassName}
+       return []string{ingressClassName}
 }
 
 func IngressServiceIndexFunc(rawObj client.Object) []string {
diff --git a/internal/controller/ingress_controller.go 
b/internal/controller/ingress_controller.go
index 5fe611da..65b1a5c7 100644
--- a/internal/controller/ingress_controller.go
+++ b/internal/controller/ingress_controller.go
@@ -237,13 +237,14 @@ func (r *IngressReconciler) 
listIngressForIngressClass(ctx context.Context, obj
                }
 
                requests := make([]reconcile.Request, 0, len(ingressList.Items))
-               for _, ingress := range ingressList.Items {
-                       if ingress.Spec.IngressClassName == nil || 
*ingress.Spec.IngressClassName == "" ||
-                               *ingress.Spec.IngressClassName == 
ingressClass.GetName() {
+               for i := range ingressList.Items {
+                       ingress := &ingressList.Items[i]
+                       effectiveClassName := 
internaltypes.GetEffectiveIngressClassName(ingress)
+                       if effectiveClassName == "" || effectiveClassName == 
ingressClass.GetName() {
                                requests = append(requests, reconcile.Request{
                                        NamespacedName: client.ObjectKey{
-                                               Namespace: ingress.Namespace,
-                                               Name:      ingress.Name,
+                                               Namespace: 
ingress.GetNamespace(),
+                                               Name:      ingress.GetName(),
                                        },
                                })
                        }
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index bed563af..845d56df 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -38,7 +38,6 @@ import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/labels"
        k8stypes "k8s.io/apimachinery/pkg/types"
-       "k8s.io/utils/ptr"
        "sigs.k8s.io/controller-runtime/pkg/client"
        "sigs.k8s.io/controller-runtime/pkg/event"
        "sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -1554,7 +1553,7 @@ func MatchesIngressClass(c client.Client, log 
logr.Logger, obj client.Object) bo
 func ExtractIngressClass(obj client.Object) string {
        switch v := obj.(type) {
        case *networkingv1.Ingress:
-               return ptr.Deref(v.Spec.IngressClassName, "")
+               return types.GetEffectiveIngressClassName(v)
        case *apiv2.ApisixConsumer:
                return v.Spec.IngressClassName
        case *apiv2.ApisixRoute:
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index 5506a426..922707d6 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -21,6 +21,7 @@ import (
        corev1 "k8s.io/api/core/v1"
        netv1 "k8s.io/api/networking/v1"
        "k8s.io/apimachinery/pkg/runtime/schema"
+       "k8s.io/utils/ptr"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
        gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
        gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -29,7 +30,10 @@ import (
        v2 "github.com/apache/apisix-ingress-controller/api/v2"
 )
 
-const DefaultIngressClassAnnotation = 
"ingressclass.kubernetes.io/is-default-class"
+const (
+       DefaultIngressClassAnnotation = 
"ingressclass.kubernetes.io/is-default-class"
+       IngressClassNameAnnotation    = "kubernetes.io/ingress.class"
+)
 
 const (
        KindGateway              = "Gateway"
@@ -198,3 +202,12 @@ func GvkOf(obj any) schema.GroupVersionKind {
                return schema.GroupVersionKind{}
        }
 }
+
+// GetEffectiveIngressClassName returns the effective ingress class name.
+// It first checks spec.IngressClassName, and falls back to the annotation if 
spec is empty.
+func GetEffectiveIngressClassName(ingress *netv1.Ingress) string {
+       if cls := ptr.Deref(ingress.Spec.IngressClassName, ""); cls != "" {
+               return cls
+       }
+       return ingress.GetAnnotations()[IngressClassNameAnnotation]
+}
diff --git a/internal/types/k8s_test.go b/internal/types/k8s_test.go
new file mode 100644
index 00000000..0260d257
--- /dev/null
+++ b/internal/types/k8s_test.go
@@ -0,0 +1,82 @@
+// 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 types
+
+import (
+       "testing"
+
+       networkingv1 "k8s.io/api/networking/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/utils/ptr"
+)
+
+func TestGetEffectiveIngressClassName(t *testing.T) {
+       tests := []struct {
+               name    string
+               ingress *networkingv1.Ingress
+               want    string
+       }{
+               {
+                       name: "spec-class",
+                       ingress: &networkingv1.Ingress{
+                               Spec: networkingv1.IngressSpec{
+                                       IngressClassName: ptr.To("spec-class"),
+                               },
+                       },
+                       want: "spec-class",
+               },
+               {
+                       name: "annotation-class",
+                       ingress: &networkingv1.Ingress{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Annotations: map[string]string{
+                                               "kubernetes.io/ingress.class": 
"annotation-class",
+                                       },
+                               },
+                       },
+                       want: "annotation-class",
+               },
+               {
+                       name: "spec-class-and-annotation-class",
+                       ingress: &networkingv1.Ingress{
+                               Spec: networkingv1.IngressSpec{
+                                       IngressClassName: ptr.To("spec-class"),
+                               },
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Annotations: map[string]string{
+                                               IngressClassNameAnnotation: 
"annotation-class",
+                                       },
+                               },
+                       },
+                       want: "spec-class",
+               },
+               {
+                       name:    "empty-ingress",
+                       ingress: &networkingv1.Ingress{},
+                       want:    "",
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := GetEffectiveIngressClassName(tt.ingress)
+                       if got != tt.want {
+                               t.Errorf("GetEffectiveIngressClassName() = %v, 
want %v", got, tt.want)
+                       }
+               })
+       }
+}
diff --git a/test/e2e/ingress/ingress.go b/test/e2e/ingress/ingress.go
index f8faccb0..12db5191 100644
--- a/test/e2e/ingress/ingress.go
+++ b/test/e2e/ingress/ingress.go
@@ -1246,4 +1246,66 @@ spec:
                                Should(Equal("enabled"))
                })
        })
+
+       Context("Ingress with annotation-based IngressClass", func() {
+               const ingressClassSpec = `
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+spec:
+  controller: "%s"
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: "apisix-proxy-config"
+    namespace: %s
+    scope: "Namespace"
+`
+               const ingressSpec = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: apisix-ingress-annotation
+  annotations:
+    kubernetes.io/ingress.class: %s
+spec:
+  rules:
+  - host: annotation.example.com
+    http:
+      paths:
+      - path: /get
+        pathType: Prefix
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+
+               It("Ingress with kubernetes.io/ingress.class annotation", 
func() {
+                       By("create GatewayProxy")
+                       gatewayProxy := fmt.Sprintf(gatewayProxyYaml, 
s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
+                       err := 
s.CreateResourceFromStringWithNamespace(gatewayProxy, s.Namespace())
+                       Expect(err).NotTo(HaveOccurred(), "creating 
GatewayProxy")
+                       time.Sleep(5 * time.Second)
+
+                       By("create IngressClass")
+                       err = 
s.CreateResourceFromStringWithNamespace(fmt.Sprintf(ingressClassSpec, 
s.Namespace(), s.GetControllerName(), s.Namespace()), s.Namespace())
+                       Expect(err).NotTo(HaveOccurred(), "creating 
IngressClass")
+
+                       By("create Ingress with annotation")
+                       err = 
s.CreateResourceFromStringWithNamespace(fmt.Sprintf(ingressSpec, 
s.Namespace()), s.Namespace())
+                       Expect(err).NotTo(HaveOccurred(), "creating Ingress 
with annotation")
+
+                       By("verify Ingress with annotation works")
+                       Eventually(func() int {
+                               return s.NewAPISIXClient().
+                                       GET("/get").
+                                       WithHost("annotation.example.com").
+                                       Expect().Raw().StatusCode
+                       }).WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).
+                               Should(Equal(http.StatusOK))
+               })
+       })
 })

Reply via email to