Skip to content

Commit cbfa038

Browse files
authored
feat: support kubernetes.io/ingress.class annotations (#2615)
Signed-off-by: Ashing Zheng <[email protected]>
1 parent 2dc7ae6 commit cbfa038

File tree

6 files changed

+168
-10
lines changed

6 files changed

+168
-10
lines changed

internal/controller/indexer/indexer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,10 +451,11 @@ func IngressClassIndexFunc(rawObj client.Object) []string {
451451

452452
func IngressClassRefIndexFunc(rawObj client.Object) []string {
453453
ingress := rawObj.(*networkingv1.Ingress)
454-
if ingress.Spec.IngressClassName == nil {
454+
ingressClassName := internaltypes.GetEffectiveIngressClassName(ingress)
455+
if ingressClassName == "" {
455456
return nil
456457
}
457-
return []string{*ingress.Spec.IngressClassName}
458+
return []string{ingressClassName}
458459
}
459460

460461
func IngressServiceIndexFunc(rawObj client.Object) []string {

internal/controller/ingress_controller.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,14 @@ func (r *IngressReconciler) listIngressForIngressClass(ctx context.Context, obj
237237
}
238238

239239
requests := make([]reconcile.Request, 0, len(ingressList.Items))
240-
for _, ingress := range ingressList.Items {
241-
if ingress.Spec.IngressClassName == nil || *ingress.Spec.IngressClassName == "" ||
242-
*ingress.Spec.IngressClassName == ingressClass.GetName() {
240+
for i := range ingressList.Items {
241+
ingress := &ingressList.Items[i]
242+
effectiveClassName := internaltypes.GetEffectiveIngressClassName(ingress)
243+
if effectiveClassName == "" || effectiveClassName == ingressClass.GetName() {
243244
requests = append(requests, reconcile.Request{
244245
NamespacedName: client.ObjectKey{
245-
Namespace: ingress.Namespace,
246-
Name: ingress.Name,
246+
Namespace: ingress.GetNamespace(),
247+
Name: ingress.GetName(),
247248
},
248249
})
249250
}

internal/controller/utils.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import (
3838
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3939
"k8s.io/apimachinery/pkg/labels"
4040
k8stypes "k8s.io/apimachinery/pkg/types"
41-
"k8s.io/utils/ptr"
4241
"sigs.k8s.io/controller-runtime/pkg/client"
4342
"sigs.k8s.io/controller-runtime/pkg/event"
4443
"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -1554,7 +1553,7 @@ func MatchesIngressClass(c client.Client, log logr.Logger, obj client.Object) bo
15541553
func ExtractIngressClass(obj client.Object) string {
15551554
switch v := obj.(type) {
15561555
case *networkingv1.Ingress:
1557-
return ptr.Deref(v.Spec.IngressClassName, "")
1556+
return types.GetEffectiveIngressClassName(v)
15581557
case *apiv2.ApisixConsumer:
15591558
return v.Spec.IngressClassName
15601559
case *apiv2.ApisixRoute:

internal/types/k8s.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
corev1 "k8s.io/api/core/v1"
2222
netv1 "k8s.io/api/networking/v1"
2323
"k8s.io/apimachinery/pkg/runtime/schema"
24+
"k8s.io/utils/ptr"
2425
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
2526
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
2627
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
@@ -29,7 +30,10 @@ import (
2930
v2 "github.com/apache/apisix-ingress-controller/api/v2"
3031
)
3132

32-
const DefaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
33+
const (
34+
DefaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
35+
IngressClassNameAnnotation = "kubernetes.io/ingress.class"
36+
)
3337

3438
const (
3539
KindGateway = "Gateway"
@@ -198,3 +202,12 @@ func GvkOf(obj any) schema.GroupVersionKind {
198202
return schema.GroupVersionKind{}
199203
}
200204
}
205+
206+
// GetEffectiveIngressClassName returns the effective ingress class name.
207+
// It first checks spec.IngressClassName, and falls back to the annotation if spec is empty.
208+
func GetEffectiveIngressClassName(ingress *netv1.Ingress) string {
209+
if cls := ptr.Deref(ingress.Spec.IngressClassName, ""); cls != "" {
210+
return cls
211+
}
212+
return ingress.GetAnnotations()[IngressClassNameAnnotation]
213+
}

internal/types/k8s_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package types
19+
20+
import (
21+
"testing"
22+
23+
networkingv1 "k8s.io/api/networking/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/utils/ptr"
26+
)
27+
28+
func TestGetEffectiveIngressClassName(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
ingress *networkingv1.Ingress
32+
want string
33+
}{
34+
{
35+
name: "spec-class",
36+
ingress: &networkingv1.Ingress{
37+
Spec: networkingv1.IngressSpec{
38+
IngressClassName: ptr.To("spec-class"),
39+
},
40+
},
41+
want: "spec-class",
42+
},
43+
{
44+
name: "annotation-class",
45+
ingress: &networkingv1.Ingress{
46+
ObjectMeta: metav1.ObjectMeta{
47+
Annotations: map[string]string{
48+
"kubernetes.io/ingress.class": "annotation-class",
49+
},
50+
},
51+
},
52+
want: "annotation-class",
53+
},
54+
{
55+
name: "spec-class-and-annotation-class",
56+
ingress: &networkingv1.Ingress{
57+
Spec: networkingv1.IngressSpec{
58+
IngressClassName: ptr.To("spec-class"),
59+
},
60+
ObjectMeta: metav1.ObjectMeta{
61+
Annotations: map[string]string{
62+
IngressClassNameAnnotation: "annotation-class",
63+
},
64+
},
65+
},
66+
want: "spec-class",
67+
},
68+
{
69+
name: "empty-ingress",
70+
ingress: &networkingv1.Ingress{},
71+
want: "",
72+
},
73+
}
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
got := GetEffectiveIngressClassName(tt.ingress)
77+
if got != tt.want {
78+
t.Errorf("GetEffectiveIngressClassName() = %v, want %v", got, tt.want)
79+
}
80+
})
81+
}
82+
}

test/e2e/ingress/ingress.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,4 +1246,66 @@ spec:
12461246
Should(Equal("enabled"))
12471247
})
12481248
})
1249+
1250+
Context("Ingress with annotation-based IngressClass", func() {
1251+
const ingressClassSpec = `
1252+
apiVersion: networking.k8s.io/v1
1253+
kind: IngressClass
1254+
metadata:
1255+
name: %s
1256+
spec:
1257+
controller: "%s"
1258+
parameters:
1259+
apiGroup: "apisix.apache.org"
1260+
kind: "GatewayProxy"
1261+
name: "apisix-proxy-config"
1262+
namespace: %s
1263+
scope: "Namespace"
1264+
`
1265+
const ingressSpec = `
1266+
apiVersion: networking.k8s.io/v1
1267+
kind: Ingress
1268+
metadata:
1269+
name: apisix-ingress-annotation
1270+
annotations:
1271+
kubernetes.io/ingress.class: %s
1272+
spec:
1273+
rules:
1274+
- host: annotation.example.com
1275+
http:
1276+
paths:
1277+
- path: /get
1278+
pathType: Prefix
1279+
backend:
1280+
service:
1281+
name: httpbin-service-e2e-test
1282+
port:
1283+
number: 80
1284+
`
1285+
1286+
It("Ingress with kubernetes.io/ingress.class annotation", func() {
1287+
By("create GatewayProxy")
1288+
gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
1289+
err := s.CreateResourceFromStringWithNamespace(gatewayProxy, s.Namespace())
1290+
Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
1291+
time.Sleep(5 * time.Second)
1292+
1293+
By("create IngressClass")
1294+
err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(ingressClassSpec, s.Namespace(), s.GetControllerName(), s.Namespace()), s.Namespace())
1295+
Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
1296+
1297+
By("create Ingress with annotation")
1298+
err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(ingressSpec, s.Namespace()), s.Namespace())
1299+
Expect(err).NotTo(HaveOccurred(), "creating Ingress with annotation")
1300+
1301+
By("verify Ingress with annotation works")
1302+
Eventually(func() int {
1303+
return s.NewAPISIXClient().
1304+
GET("/get").
1305+
WithHost("annotation.example.com").
1306+
Expect().Raw().StatusCode
1307+
}).WithTimeout(20 * time.Second).ProbeEvery(time.Second).
1308+
Should(Equal(http.StatusOK))
1309+
})
1310+
})
12491311
})

0 commit comments

Comments
 (0)