diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index f94faafc..2c2d3192 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -444,10 +444,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 ae20e539..e0a047ec 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -254,13 +254,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 72b8ab59..70d0f801 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -41,7 +41,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" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" @@ -1718,7 +1717,7 @@ func MatchesIngressClass(c client.Client, log logr.Logger, obj client.Object, ap 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 047fd2a8..0c55879c 100644 --- a/internal/types/k8s.go +++ b/internal/types/k8s.go @@ -23,6 +23,7 @@ import ( netv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" kschema "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -31,7 +32,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" @@ -204,3 +208,10 @@ func GvkOf(obj any) schema.GroupVersionKind { return schema.GroupVersionKind{} } } + +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 b61e3109..06886716 100644 --- a/test/e2e/ingress/ingress.go +++ b/test/e2e/ingress/ingress.go @@ -380,6 +380,54 @@ spec: }) }) + Context("Ingress Annotation", func() { + var ingressSpecWithAnnotation = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: %s + annotations: + kubernetes.io/ingress.class: %s +spec: + rules: + - host: annotation.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + + It("kubernetes.io/ingress.class", func() { + By("create GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), s.Namespace()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create Ingress with annotation-defined class") + ingressName := s.Namespace() + "-annotation" + err = s.CreateResourceFromString(fmt.Sprintf(ingressSpecWithAnnotation, ingressName, s.Namespace())) + Expect(err).NotTo(HaveOccurred(), "creating Ingress with kubernetes.io/ingress.class annotation") + + By("verify ingress served through annotated class") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "annotation.example.com", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + }) + }) + // Tests concerning the default ingress class need to be run serially Context("IngressClass with GatewayProxy", Serial, func() { gatewayProxyYaml := `