Skip to content

Commit b21b6e9

Browse files
AlinsRanronething
andauthored
feat: support regex route for ingress annotations (#2640)
Co-authored-by: Ashing Zheng <[email protected]>
1 parent e7d192b commit b21b6e9

File tree

8 files changed

+143
-176
lines changed

8 files changed

+143
-176
lines changed

internal/adc/translator/annotations.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
2626
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/pluginconfig"
2727
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins"
28+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/regex"
2829
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/servicenamespace"
2930
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
3031
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/websocket"
@@ -37,6 +38,7 @@ type IngressConfig struct {
3738
EnableWebsocket bool
3839
ServiceNamespace string
3940
PluginConfigName string
41+
UseRegex bool
4042
}
4143

4244
var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
@@ -45,6 +47,7 @@ var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
4547
"EnableWebsocket": websocket.NewParser(),
4648
"PluginConfigName": pluginconfig.NewParser(),
4749
"ServiceNamespace": servicenamespace.NewParser(),
50+
"UseRegex": regex.NewParser(),
4851
}
4952

5053
func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
package regex
16+
17+
import (
18+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
19+
)
20+
21+
type regex struct{}
22+
23+
func NewParser() annotations.IngressAnnotationsParser {
24+
return &regex{}
25+
}
26+
27+
func (r *regex) Parse(e annotations.Extractor) (any, error) {
28+
return e.GetBoolAnnotation(annotations.AnnotationsUseRegex), nil
29+
}

internal/adc/translator/annotations_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,15 @@ func TestTranslateIngressAnnotations(t *testing.T) {
329329
},
330330
},
331331
},
332+
{
333+
name: "regex",
334+
anno: map[string]string{
335+
annotations.AnnotationsUseRegex: "true",
336+
},
337+
expected: &IngressConfig{
338+
UseRegex: true,
339+
},
340+
},
332341
}
333342

334343
for _, tt := range tests {

internal/adc/translator/ingress.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/utils/ptr"
3131

3232
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
33+
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
3334
"github.com/apache/apisix-ingress-controller/internal/controller/label"
3435
"github.com/apache/apisix-ingress-controller/internal/id"
3536
"github.com/apache/apisix-ingress-controller/internal/provider"
@@ -281,7 +282,24 @@ func (t *Translator) buildRouteFromIngressPath(
281282
prefix := strings.TrimSuffix(path.Path, "/") + "/*"
282283
uris = append(uris, prefix)
283284
case networkingv1.PathTypeImplementationSpecific:
284-
uris = []string{"/*"}
285+
if config.UseRegex {
286+
uris = []string{"/*"}
287+
vars := apiv2.ApisixRouteHTTPMatchExprs{
288+
apiv2.ApisixRouteHTTPMatchExpr{
289+
Subject: apiv2.ApisixRouteHTTPMatchExprSubject{
290+
Scope: apiv2.ScopePath,
291+
},
292+
Op: apiv2.OpRegexMatch,
293+
Value: &path.Path,
294+
},
295+
}
296+
routeVars, err := vars.ToVars()
297+
if err != nil {
298+
t.Log.Error(err, "failed to convert regex match exprs to vars", "exprs", vars)
299+
} else {
300+
route.Vars = append(route.Vars, routeVars...)
301+
}
302+
}
285303
}
286304
}
287305

internal/webhook/v1/ingress_webhook.go

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package v1
1818
import (
1919
"context"
2020
"fmt"
21-
"slices"
2221

2322
networkingv1 "k8s.io/api/networking/v1"
2423
"k8s.io/apimachinery/pkg/runtime"
@@ -36,36 +35,6 @@ import (
3635

3736
var ingresslog = logf.Log.WithName("ingress-resource")
3837

39-
// unsupportedAnnotations contains all the APISIX Ingress annotations that are not supported in 2.0.0
40-
// ref: https://apisix.apache.org/docs/ingress-controller/upgrade-guide/#limited-support-for-ingress-annotations
41-
var unsupportedAnnotations = []string{
42-
"k8s.apisix.apache.org/use-regex",
43-
"k8s.apisix.apache.org/auth-type",
44-
}
45-
46-
// checkUnsupportedAnnotations checks if the Ingress contains any unsupported annotations
47-
// and returns appropriate warnings
48-
func checkUnsupportedAnnotations(ingress *networkingv1.Ingress) admission.Warnings {
49-
var warnings admission.Warnings
50-
51-
if len(ingress.Annotations) == 0 {
52-
return warnings
53-
}
54-
55-
for annotation := range ingress.Annotations {
56-
if slices.Contains(unsupportedAnnotations, annotation) {
57-
warningMsg := fmt.Sprintf("Annotation '%s' is not supported in APISIX Ingress Controller 2.0.0.", annotation)
58-
warnings = append(warnings, warningMsg)
59-
ingresslog.Info("Detected unsupported annotation",
60-
"ingress", ingress.GetName(),
61-
"namespace", ingress.GetNamespace(),
62-
"annotation", annotation)
63-
}
64-
}
65-
66-
return warnings
67-
}
68-
6938
// SetupIngressWebhookWithManager registers the webhook for Ingress in the manager.
7039
func SetupIngressWebhookWithManager(mgr ctrl.Manager) error {
7140
return ctrl.NewWebhookManagedBy(mgr).For(&networkingv1.Ingress{}).
@@ -113,10 +82,7 @@ func (v *IngressCustomValidator) ValidateCreate(ctx context.Context, obj runtime
11382
return nil, fmt.Errorf("%s", sslvalidator.FormatConflicts(conflicts))
11483
}
11584

116-
// Check for unsupported annotations and generate warnings
117-
warnings := checkUnsupportedAnnotations(ingress)
118-
warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
119-
85+
warnings := v.collectReferenceWarnings(ctx, ingress)
12086
return warnings, nil
12187
}
12288

@@ -137,9 +103,7 @@ func (v *IngressCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new
137103
return nil, fmt.Errorf("%s", sslvalidator.FormatConflicts(conflicts))
138104
}
139105

140-
// Check for unsupported annotations and generate warnings
141-
warnings := checkUnsupportedAnnotations(ingress)
142-
warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
106+
warnings := v.collectReferenceWarnings(ctx, ingress)
143107
return warnings, nil
144108
}
145109

internal/webhook/v1/ingress_webhook_test.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -63,54 +63,6 @@ func buildIngressValidator(t *testing.T, objects ...runtime.Object) *IngressCust
6363
return NewIngressCustomValidator(builder.Build())
6464
}
6565

66-
func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T) {
67-
validator := buildIngressValidator(t)
68-
obj := &networkingv1.Ingress{
69-
ObjectMeta: metav1.ObjectMeta{
70-
Name: "test-ingress",
71-
Namespace: "default",
72-
Annotations: map[string]string{
73-
"ingressclass.kubernetes.io/is-default-class": "true",
74-
},
75-
},
76-
}
77-
78-
warnings, err := validator.ValidateCreate(context.TODO(), obj)
79-
assert.NoError(t, err)
80-
assert.Empty(t, warnings)
81-
}
82-
83-
func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) {
84-
validator := buildIngressValidator(t)
85-
obj := &networkingv1.Ingress{
86-
ObjectMeta: metav1.ObjectMeta{
87-
Name: "test-ingress",
88-
Namespace: "default",
89-
Annotations: map[string]string{
90-
"k8s.apisix.apache.org/use-regex": "true",
91-
},
92-
},
93-
}
94-
95-
warnings, err := validator.ValidateDelete(context.TODO(), obj)
96-
assert.NoError(t, err)
97-
assert.Empty(t, warnings)
98-
}
99-
100-
func TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) {
101-
validator := buildIngressValidator(t)
102-
obj := &networkingv1.Ingress{
103-
ObjectMeta: metav1.ObjectMeta{
104-
Name: "test-ingress",
105-
Namespace: "default",
106-
},
107-
}
108-
109-
warnings, err := validator.ValidateCreate(context.TODO(), obj)
110-
assert.NoError(t, err)
111-
assert.Empty(t, warnings)
112-
}
113-
11466
func TestIngressCustomValidator_WarnsForMissingServiceAndSecret(t *testing.T) {
11567
validator := buildIngressValidator(t)
11668
obj := &networkingv1.Ingress{

test/e2e/ingress/annotations.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,4 +1144,85 @@ spec:
11441144
})
11451145
})
11461146
})
1147+
1148+
Context("Route", func() {
1149+
var (
1150+
ingressRegex = `
1151+
apiVersion: networking.k8s.io/v1
1152+
kind: Ingress
1153+
metadata:
1154+
name: regex
1155+
annotations:
1156+
k8s.apisix.apache.org/use-regex: "true"
1157+
spec:
1158+
ingressClassName: %s
1159+
rules:
1160+
- host: httpbin.example
1161+
http:
1162+
paths:
1163+
- path: /anything/.*/ok
1164+
pathType: ImplementationSpecific
1165+
backend:
1166+
service:
1167+
name: httpbin-service-e2e-test
1168+
port:
1169+
number: 80
1170+
`
1171+
)
1172+
BeforeEach(func() {
1173+
By("create GatewayProxy")
1174+
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")
1175+
1176+
By("create IngressClass")
1177+
err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
1178+
Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
1179+
time.Sleep(5 * time.Second)
1180+
})
1181+
It("regex match", func() {
1182+
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRegex, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
1183+
1184+
tests := []*scaffold.RequestAssert{
1185+
{
1186+
Method: "GET",
1187+
Path: "/anything/test/ok",
1188+
Host: "httpbin.example",
1189+
Check: scaffold.WithExpectedStatus(http.StatusOK),
1190+
},
1191+
{
1192+
Method: "GET",
1193+
Path: "/anything/ip/ok",
1194+
Host: "httpbin.example",
1195+
Check: scaffold.WithExpectedStatus(http.StatusOK),
1196+
},
1197+
{
1198+
Method: "GET",
1199+
Path: "/test/notok",
1200+
Host: "httpbin.example",
1201+
Check: scaffold.WithExpectedStatus(http.StatusNotFound),
1202+
},
1203+
1204+
{
1205+
Method: "GET",
1206+
Path: "/anything",
1207+
Host: "httpbin.example",
1208+
Check: scaffold.WithExpectedStatus(http.StatusNotFound),
1209+
},
1210+
{
1211+
Method: "GET",
1212+
Path: "/anything/test/notok",
1213+
Host: "httpbin.example",
1214+
Check: scaffold.WithExpectedStatus(http.StatusNotFound),
1215+
},
1216+
{
1217+
Method: "GET",
1218+
Path: "/anything/ok",
1219+
Host: "httpbin.example",
1220+
Check: scaffold.WithExpectedStatus(http.StatusNotFound),
1221+
},
1222+
}
1223+
for _, test := range tests {
1224+
s.RequestAssert(test)
1225+
}
1226+
})
1227+
})
11471228
})

0 commit comments

Comments
 (0)