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)) + }) + }) })
