diff --git a/internal/adc/translator/annotations.go b/internal/adc/translator/annotations.go index 9015a08821..49486dc38c 100644 --- a/internal/adc/translator/annotations.go +++ b/internal/adc/translator/annotations.go @@ -25,6 +25,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/pluginconfig" "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/regex" "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/servicenamespace" "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream" "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/websocket" @@ -37,6 +38,7 @@ type IngressConfig struct { EnableWebsocket bool ServiceNamespace string PluginConfigName string + UseRegex bool } var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{ @@ -45,6 +47,7 @@ var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{ "EnableWebsocket": websocket.NewParser(), "PluginConfigName": pluginconfig.NewParser(), "ServiceNamespace": servicenamespace.NewParser(), + "UseRegex": regex.NewParser(), } func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig { diff --git a/internal/adc/translator/annotations/regex/regex.go b/internal/adc/translator/annotations/regex/regex.go new file mode 100644 index 0000000000..0b659805fa --- /dev/null +++ b/internal/adc/translator/annotations/regex/regex.go @@ -0,0 +1,29 @@ +// 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 regex + +import ( + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +type regex struct{} + +func NewParser() annotations.IngressAnnotationsParser { + return ®ex{} +} + +func (r *regex) Parse(e annotations.Extractor) (any, error) { + return e.GetBoolAnnotation(annotations.AnnotationsUseRegex), nil +} diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go index 6fe51da961..8c8b1b96ef 100644 --- a/internal/adc/translator/annotations_test.go +++ b/internal/adc/translator/annotations_test.go @@ -329,6 +329,15 @@ func TestTranslateIngressAnnotations(t *testing.T) { }, }, }, + { + name: "regex", + anno: map[string]string{ + annotations.AnnotationsUseRegex: "true", + }, + expected: &IngressConfig{ + UseRegex: true, + }, + }, } for _, tt := range tests { diff --git a/internal/adc/translator/ingress.go b/internal/adc/translator/ingress.go index efb12cf71d..c1bae046b6 100644 --- a/internal/adc/translator/ingress.go +++ b/internal/adc/translator/ingress.go @@ -30,6 +30,7 @@ import ( "k8s.io/utils/ptr" adctypes "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/controller/label" "github.com/apache/apisix-ingress-controller/internal/id" "github.com/apache/apisix-ingress-controller/internal/provider" @@ -281,7 +282,24 @@ func (t *Translator) buildRouteFromIngressPath( prefix := strings.TrimSuffix(path.Path, "/") + "/*" uris = append(uris, prefix) case networkingv1.PathTypeImplementationSpecific: - uris = []string{"/*"} + if config.UseRegex { + uris = []string{"/*"} + vars := apiv2.ApisixRouteHTTPMatchExprs{ + apiv2.ApisixRouteHTTPMatchExpr{ + Subject: apiv2.ApisixRouteHTTPMatchExprSubject{ + Scope: apiv2.ScopePath, + }, + Op: apiv2.OpRegexMatch, + Value: &path.Path, + }, + } + routeVars, err := vars.ToVars() + if err != nil { + t.Log.Error(err, "failed to convert regex match exprs to vars", "exprs", vars) + } else { + route.Vars = append(route.Vars, routeVars...) + } + } } } diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go index 2dfc6de59c..292e2043cd 100644 --- a/internal/webhook/v1/ingress_webhook.go +++ b/internal/webhook/v1/ingress_webhook.go @@ -18,7 +18,6 @@ package v1 import ( "context" "fmt" - "slices" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/runtime" @@ -36,36 +35,6 @@ import ( var ingresslog = logf.Log.WithName("ingress-resource") -// unsupportedAnnotations contains all the APISIX Ingress annotations that are not supported in 2.0.0 -// ref: https://apisix.apache.org/docs/ingress-controller/upgrade-guide/#limited-support-for-ingress-annotations -var unsupportedAnnotations = []string{ - "k8s.apisix.apache.org/use-regex", - "k8s.apisix.apache.org/auth-type", -} - -// checkUnsupportedAnnotations checks if the Ingress contains any unsupported annotations -// and returns appropriate warnings -func checkUnsupportedAnnotations(ingress *networkingv1.Ingress) admission.Warnings { - var warnings admission.Warnings - - if len(ingress.Annotations) == 0 { - return warnings - } - - for annotation := range ingress.Annotations { - if slices.Contains(unsupportedAnnotations, annotation) { - warningMsg := fmt.Sprintf("Annotation '%s' is not supported in APISIX Ingress Controller 2.0.0.", annotation) - warnings = append(warnings, warningMsg) - ingresslog.Info("Detected unsupported annotation", - "ingress", ingress.GetName(), - "namespace", ingress.GetNamespace(), - "annotation", annotation) - } - } - - return warnings -} - // SetupIngressWebhookWithManager registers the webhook for Ingress in the manager. func SetupIngressWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&networkingv1.Ingress{}). @@ -113,10 +82,7 @@ func (v *IngressCustomValidator) ValidateCreate(ctx context.Context, obj runtime return nil, fmt.Errorf("%s", sslvalidator.FormatConflicts(conflicts)) } - // Check for unsupported annotations and generate warnings - warnings := checkUnsupportedAnnotations(ingress) - warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...) - + warnings := v.collectReferenceWarnings(ctx, ingress) return warnings, nil } @@ -137,9 +103,7 @@ func (v *IngressCustomValidator) ValidateUpdate(ctx context.Context, oldObj, new return nil, fmt.Errorf("%s", sslvalidator.FormatConflicts(conflicts)) } - // Check for unsupported annotations and generate warnings - warnings := checkUnsupportedAnnotations(ingress) - warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...) + warnings := v.collectReferenceWarnings(ctx, ingress) return warnings, nil } diff --git a/internal/webhook/v1/ingress_webhook_test.go b/internal/webhook/v1/ingress_webhook_test.go index 7394ef4b21..d6bce33fdf 100644 --- a/internal/webhook/v1/ingress_webhook_test.go +++ b/internal/webhook/v1/ingress_webhook_test.go @@ -63,54 +63,6 @@ func buildIngressValidator(t *testing.T, objects ...runtime.Object) *IngressCust return NewIngressCustomValidator(builder.Build()) } -func TestIngressCustomValidator_ValidateCreate_SupportedAnnotations(t *testing.T) { - validator := buildIngressValidator(t) - obj := &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ingress", - Namespace: "default", - Annotations: map[string]string{ - "ingressclass.kubernetes.io/is-default-class": "true", - }, - }, - } - - warnings, err := validator.ValidateCreate(context.TODO(), obj) - assert.NoError(t, err) - assert.Empty(t, warnings) -} - -func TestIngressCustomValidator_ValidateDelete_NoWarnings(t *testing.T) { - validator := buildIngressValidator(t) - obj := &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ingress", - Namespace: "default", - Annotations: map[string]string{ - "k8s.apisix.apache.org/use-regex": "true", - }, - }, - } - - warnings, err := validator.ValidateDelete(context.TODO(), obj) - assert.NoError(t, err) - assert.Empty(t, warnings) -} - -func TestIngressCustomValidator_ValidateCreate_NoAnnotations(t *testing.T) { - validator := buildIngressValidator(t) - obj := &networkingv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ingress", - Namespace: "default", - }, - } - - warnings, err := validator.ValidateCreate(context.TODO(), obj) - assert.NoError(t, err) - assert.Empty(t, warnings) -} - func TestIngressCustomValidator_WarnsForMissingServiceAndSecret(t *testing.T) { validator := buildIngressValidator(t) obj := &networkingv1.Ingress{ diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go index b29aeaee56..a65cddbc20 100644 --- a/test/e2e/ingress/annotations.go +++ b/test/e2e/ingress/annotations.go @@ -1144,4 +1144,85 @@ spec: }) }) }) + + Context("Route", func() { + var ( + ingressRegex = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: regex + annotations: + k8s.apisix.apache.org/use-regex: "true" +spec: + ingressClassName: %s + rules: + - host: httpbin.example + http: + paths: + - path: /anything/.*/ok + pathType: ImplementationSpecific + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + ) + BeforeEach(func() { + By("create GatewayProxy") + Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy") + + By("create IngressClass") + err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + It("regex match", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRegex, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + tests := []*scaffold.RequestAssert{ + { + Method: "GET", + Path: "/anything/test/ok", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }, + { + Method: "GET", + Path: "/anything/ip/ok", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }, + { + Method: "GET", + Path: "/test/notok", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + }, + + { + Method: "GET", + Path: "/anything", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + }, + { + Method: "GET", + Path: "/anything/test/notok", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + }, + { + Method: "GET", + Path: "/anything/ok", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + }, + } + for _, test := range tests { + s.RequestAssert(test) + } + }) + }) }) diff --git a/test/e2e/webhook/ingress.go b/test/e2e/webhook/ingress.go deleted file mode 100644 index cf3e519981..0000000000 --- a/test/e2e/webhook/ingress.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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 webhook - -import ( - "fmt" - "net/http" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" -) - -var _ = Describe("Test Ingress Webhook", Label("webhook"), func() { - s := scaffold.NewScaffold(scaffold.Options{ - Name: "webhook-test", - EnableWebhook: true, - }) - - BeforeEach(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(), "") - Expect(err).NotTo(HaveOccurred(), "creating IngressClass") - time.Sleep(5 * time.Second) - }) - - Context("Ingress Validation", func() { - It("should warn about unsupported annotations on create", func() { - - By("creating Ingress with unsupported annotations") - ingressYAML := fmt.Sprintf(` -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: test-webhook-unsupported - namespace: %s - annotations: - k8s.apisix.apache.org/use-regex: "true" -spec: - ingressClassName: %s - rules: - - host: webhook-test.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: httpbin-service-e2e-test - port: - number: 80 -`, s.Namespace(), s.Namespace()) - - output, err := s.CreateResourceFromStringAndGetOutput(ingressYAML) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(`Warning: Annotation 'k8s.apisix.apache.org/use-regex' is not supported`)) - - s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "webhook-test.example.com", - Check: scaffold.WithExpectedStatus(http.StatusOK), - }) - }) - - }) -})