This is an automated email from the ASF dual-hosted git repository. ronething pushed a commit to branch feat/proxy-rewrite in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
commit e051295a9ab1748ae73903329c6bc1887d765a85 Author: Ashing Zheng <[email protected]> AuthorDate: Wed Oct 29 16:33:49 2025 +0800 feat: support proxy rewrite annotations for ingress Signed-off-by: Ashing Zheng <[email protected]> --- .../adc/translator/annotations/plugins/plugins.go | 1 + .../adc/translator/annotations/plugins/rewrite.go | 60 ++++++++++++ .../translator/annotations/plugins/rewrite_test.go | 87 +++++++++++++++++ internal/webhook/v1/ingress_webhook.go | 3 - test/e2e/ingress/annotations.go | 107 +++++++++++++++++++++ 5 files changed, 255 insertions(+), 3 deletions(-) diff --git a/internal/adc/translator/annotations/plugins/plugins.go b/internal/adc/translator/annotations/plugins/plugins.go index 2dd8e5f8..935e70e7 100644 --- a/internal/adc/translator/annotations/plugins/plugins.go +++ b/internal/adc/translator/annotations/plugins/plugins.go @@ -37,6 +37,7 @@ var ( handlers = []PluginAnnotationsHandler{ NewRedirectHandler(), + NewRewriteHandler(), NewCorsHandler(), NewCSRFHandler(), NewFaultInjectionHandler(), diff --git a/internal/adc/translator/annotations/plugins/rewrite.go b/internal/adc/translator/annotations/plugins/rewrite.go new file mode 100644 index 00000000..30edfd27 --- /dev/null +++ b/internal/adc/translator/annotations/plugins/rewrite.go @@ -0,0 +1,60 @@ +// 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 plugins + +import ( + "regexp" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +type rewrite struct{} + +// NewRewriteHandler creates a handler to convert +// annotations about request rewrite control to APISIX proxy-rewrite plugin. +func NewRewriteHandler() PluginAnnotationsHandler { + return &rewrite{} +} + +func (r *rewrite) PluginName() string { + return "proxy-rewrite" +} + +func (r *rewrite) Handle(e annotations.Extractor) (any, error) { + rewriteTarget := e.GetStringAnnotation(annotations.AnnotationsRewriteTarget) + rewriteTargetRegex := e.GetStringAnnotation(annotations.AnnotationsRewriteTargetRegex) + rewriteTemplate := e.GetStringAnnotation(annotations.AnnotationsRewriteTargetRegexTemplate) + + // If no rewrite annotations are present, return nil + if rewriteTarget == "" && rewriteTargetRegex == "" && rewriteTemplate == "" { + return nil, nil + } + + var plugin adctypes.RewriteConfig + plugin.RewriteTarget = rewriteTarget + + // If both regex and template are provided, validate and set regex_uri + if rewriteTargetRegex != "" && rewriteTemplate != "" { + _, err := regexp.Compile(rewriteTargetRegex) + if err != nil { + return nil, err + } + plugin.RewriteTargetRegex = []string{rewriteTargetRegex, rewriteTemplate} + } + + return &plugin, nil +} diff --git a/internal/adc/translator/annotations/plugins/rewrite_test.go b/internal/adc/translator/annotations/plugins/rewrite_test.go new file mode 100644 index 00000000..99f89c6c --- /dev/null +++ b/internal/adc/translator/annotations/plugins/rewrite_test.go @@ -0,0 +1,87 @@ +// 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 plugins + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +func TestRewriteHandler(t *testing.T) { + t.Run("rewrite target", func(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsRewriteTarget: "/new-path", + } + p := NewRewriteHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + assert.NotNil(t, out, "checking given output") + config := out.(*adctypes.RewriteConfig) + assert.Equal(t, "/new-path", config.RewriteTarget) + assert.Nil(t, config.RewriteTargetRegex) + assert.Equal(t, "proxy-rewrite", p.PluginName()) + }) + + t.Run("rewrite target with regex", func(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsRewriteTargetRegex: "/sample/(.*)", + annotations.AnnotationsRewriteTargetRegexTemplate: "/$1", + } + p := NewRewriteHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + assert.NotNil(t, out, "checking given output") + config := out.(*adctypes.RewriteConfig) + assert.Equal(t, "", config.RewriteTarget) + assert.NotNil(t, config.RewriteTargetRegex) + assert.Equal(t, []string{"/sample/(.*)", "/$1"}, config.RewriteTargetRegex) + }) + + t.Run("invalid regex", func(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsRewriteTargetRegex: "[invalid(regex", + annotations.AnnotationsRewriteTargetRegexTemplate: "/$1", + } + p := NewRewriteHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.NotNil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") + }) + + t.Run("no annotations", func(t *testing.T) { + anno := map[string]string{} + p := NewRewriteHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + assert.Nil(t, out, "checking given output") + }) + + t.Run("only regex without template", func(t *testing.T) { + anno := map[string]string{ + annotations.AnnotationsRewriteTargetRegex: "/sample/(.*)", + } + p := NewRewriteHandler() + out, err := p.Handle(annotations.NewExtractor(anno)) + assert.Nil(t, err, "checking given error") + assert.NotNil(t, out, "checking given output") + config := out.(*adctypes.RewriteConfig) + assert.Nil(t, config.RewriteTargetRegex, "regex should not be set without template") + }) +} diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go index 78bb4908..6f72451f 100644 --- a/internal/webhook/v1/ingress_webhook.go +++ b/internal/webhook/v1/ingress_webhook.go @@ -40,9 +40,6 @@ var ingresslog = logf.Log.WithName("ingress-resource") // 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/rewrite-target", - "k8s.apisix.apache.org/rewrite-target-regex", - "k8s.apisix.apache.org/rewrite-target-regex-template", "k8s.apisix.apache.org/enable-response-rewrite", "k8s.apisix.apache.org/response-rewrite-status-code", "k8s.apisix.apache.org/response-rewrite-body", diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go index ad559528..b1c452ff 100644 --- a/test/e2e/ingress/annotations.go +++ b/test/e2e/ingress/annotations.go @@ -383,6 +383,51 @@ spec: port: number: 80 ` + + ingressRewriteTarget = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rewrite-target + annotations: + k8s.apisix.apache.org/rewrite-target: "/get" +spec: + ingressClassName: %s + rules: + - host: httpbin.example + http: + paths: + - path: /test + pathType: Exact + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + + ingressRewriteTargetRegex = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rewrite-target-regex + annotations: + k8s.apisix.apache.org/rewrite-target-regex: "/sample/(.*)" + k8s.apisix.apache.org/rewrite-target-regex-template: "/$1" +spec: + ingressClassName: %s + rules: + - host: httpbin-regex.example + http: + paths: + - path: /sample + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` ) BeforeEach(func() { By("create GatewayProxy") @@ -610,5 +655,67 @@ spec: s.RequestAssert(test) } }) + + It("proxy-rewrite with rewrite-target", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRewriteTarget, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + time.Sleep(5 * time.Second) + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/test", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + routes, err := s.DefaultDataplaneResource().Route().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Route") + Expect(routes).ToNot(BeEmpty(), "checking Route length") + Expect(routes[0].Plugins).To(HaveKey("proxy-rewrite"), "checking Route has proxy-rewrite plugin") + + jsonBytes, err := json.Marshal(routes[0].Plugins["proxy-rewrite"]) + Expect(err).NotTo(HaveOccurred(), "marshalling proxy-rewrite plugin config") + var rewriteConfig map[string]any + err = json.Unmarshal(jsonBytes, &rewriteConfig) + Expect(err).NotTo(HaveOccurred(), "unmarshalling proxy-rewrite plugin config") + Expect(rewriteConfig["uri"]).To(Equal("/get"), "checking proxy-rewrite uri") + }) + + It("proxy-rewrite with regex", func() { + Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRewriteTargetRegex, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress") + + time.Sleep(5 * time.Second) + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/sample/get", + Host: "httpbin-regex.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/sample/anything", + Host: "httpbin-regex.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + routes, err := s.DefaultDataplaneResource().Route().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "listing Route") + Expect(routes).ToNot(BeEmpty(), "checking Route length") + Expect(routes[0].Plugins).To(HaveKey("proxy-rewrite"), "checking Route has proxy-rewrite plugin") + + jsonBytes, err := json.Marshal(routes[0].Plugins["proxy-rewrite"]) + Expect(err).NotTo(HaveOccurred(), "marshalling proxy-rewrite plugin config") + var rewriteConfig map[string]any + err = json.Unmarshal(jsonBytes, &rewriteConfig) + Expect(err).NotTo(HaveOccurred(), "unmarshalling proxy-rewrite plugin config") + + regexUri, ok := rewriteConfig["regex_uri"].([]any) + Expect(ok).To(BeTrue(), "checking regex_uri is array") + Expect(regexUri).To(HaveLen(2), "checking regex_uri length") + Expect(regexUri[0]).To(Equal("/sample/(.*)"), "checking regex pattern") + Expect(regexUri[1]).To(Equal("/$1"), "checking regex template") + }) }) })
