This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 9be64b6a feat: support response rewrite annotations for ingress (#2638)
9be64b6a is described below

commit 9be64b6abf273e3912f708012e712236050dd5fd
Author: Ashing Zheng <[email protected]>
AuthorDate: Thu Oct 30 16:30:14 2025 +0800

    feat: support response rewrite annotations for ingress (#2638)
---
 .../adc/translator/annotations/plugins/plugins.go  |   1 +
 .../annotations/plugins/response_rewrite.go        |  89 +++++++++++++
 .../annotations/plugins/response_rewrite_test.go   | 139 +++++++++++++++++++++
 internal/webhook/v1/ingress_webhook.go             |   7 --
 test/e2e/ingress/annotations.go                    | 108 ++++++++++++++++
 5 files changed, 337 insertions(+), 7 deletions(-)

diff --git a/internal/adc/translator/annotations/plugins/plugins.go 
b/internal/adc/translator/annotations/plugins/plugins.go
index a8e71239..06bc87f4 100644
--- a/internal/adc/translator/annotations/plugins/plugins.go
+++ b/internal/adc/translator/annotations/plugins/plugins.go
@@ -43,6 +43,7 @@ var (
                NewFaultInjectionHandler(),
                NewBasicAuthHandler(),
                NewKeyAuthHandler(),
+               NewResponseRewriteHandler(),
        }
 )
 
diff --git a/internal/adc/translator/annotations/plugins/response_rewrite.go 
b/internal/adc/translator/annotations/plugins/response_rewrite.go
new file mode 100644
index 00000000..7de62238
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/response_rewrite.go
@@ -0,0 +1,89 @@
+// 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 (
+       "strconv"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+type responseRewrite struct{}
+
+// NewResponseRewriteHandler creates a handler to convert annotations about
+// ResponseRewrite to APISIX response-rewrite plugin.
+func NewResponseRewriteHandler() PluginAnnotationsHandler {
+       return &responseRewrite{}
+}
+
+func (r *responseRewrite) PluginName() string {
+       return "response-rewrite"
+}
+
+func (r *responseRewrite) Handle(e annotations.Extractor) (any, error) {
+       if !e.GetBoolAnnotation(annotations.AnnotationsEnableResponseRewrite) {
+               return nil, nil
+       }
+
+       plugin := &adctypes.ResponseRewriteConfig{
+               BodyBase64: 
e.GetBoolAnnotation(annotations.AnnotationsResponseRewriteBodyBase64),
+               Body:       
e.GetStringAnnotation(annotations.AnnotationsResponseRewriteBody),
+       }
+
+       // Parse status code, transformation fail defaults to 0
+       if statusCodeStr := 
e.GetStringAnnotation(annotations.AnnotationsResponseRewriteStatusCode); 
statusCodeStr != "" {
+               if statusCode, err := strconv.Atoi(statusCodeStr); err == nil {
+                       plugin.StatusCode = statusCode
+               }
+       }
+
+       // Handle headers
+       addHeaders := 
e.GetStringsAnnotation(annotations.AnnotationsResponseRewriteHeaderAdd)
+       setHeaders := 
e.GetStringsAnnotation(annotations.AnnotationsResponseRewriteHeaderSet)
+       removeHeaders := 
e.GetStringsAnnotation(annotations.AnnotationsResponseRewriteHeaderRemove)
+
+       if len(addHeaders) > 0 || len(setHeaders) > 0 || len(removeHeaders) > 0 
{
+               headers := &adctypes.ResponseHeaders{
+                       Add:    addHeaders,
+                       Remove: removeHeaders,
+               }
+
+               // Convert set headers from ["key:value", ...] to 
map[string]string
+               if len(setHeaders) > 0 {
+                       headers.Set = make(map[string]string)
+                       for _, header := range setHeaders {
+                               if key, value, found := 
parseHeaderKeyValue(header); found {
+                                       headers.Set[key] = value
+                               }
+                       }
+               }
+
+               plugin.Headers = headers
+       }
+
+       return plugin, nil
+}
+
+// parseHeaderKeyValue parses a header string in format "key:value" and 
returns key, value and success flag
+func parseHeaderKeyValue(header string) (string, string, bool) {
+       for i := 0; i < len(header); i++ {
+               if header[i] == ':' {
+                       return header[:i], header[i+1:], true
+               }
+       }
+       return "", "", false
+}
diff --git 
a/internal/adc/translator/annotations/plugins/response_rewrite_test.go 
b/internal/adc/translator/annotations/plugins/response_rewrite_test.go
new file mode 100644
index 00000000..1f7521b7
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/response_rewrite_test.go
@@ -0,0 +1,139 @@
+// 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 TestResponseRewriteHandler(t *testing.T) {
+       anno := map[string]string{
+               annotations.AnnotationsEnableResponseRewrite:       "true",
+               annotations.AnnotationsResponseRewriteStatusCode:   "200",
+               annotations.AnnotationsResponseRewriteBody:         "bar_body",
+               annotations.AnnotationsResponseRewriteBodyBase64:   "false",
+               annotations.AnnotationsResponseRewriteHeaderAdd:    
"testkey1:testval1,testkey2:testval2",
+               annotations.AnnotationsResponseRewriteHeaderRemove: 
"testkey1,testkey2",
+               annotations.AnnotationsResponseRewriteHeaderSet:    
"testkey1:testval1,testkey2:testval2",
+       }
+       p := NewResponseRewriteHandler()
+       out, err := p.Handle(annotations.NewExtractor(anno))
+       assert.Nil(t, err, "checking given error")
+       config := out.(*adctypes.ResponseRewriteConfig)
+       assert.Equal(t, 200, config.StatusCode)
+       assert.Equal(t, "bar_body", config.Body)
+       assert.Equal(t, false, config.BodyBase64)
+       assert.Equal(t, "response-rewrite", p.PluginName())
+       assert.Equal(t, []string{"testkey1:testval1", "testkey2:testval2"}, 
config.Headers.Add)
+       assert.Equal(t, []string{"testkey1", "testkey2"}, config.Headers.Remove)
+       assert.Equal(t, map[string]string{
+               "testkey1": "testval1",
+               "testkey2": "testval2",
+       }, config.Headers.Set)
+}
+
+func TestResponseRewriteHandlerDisabled(t *testing.T) {
+       anno := map[string]string{
+               annotations.AnnotationsEnableResponseRewrite:     "false",
+               annotations.AnnotationsResponseRewriteStatusCode: "400",
+               annotations.AnnotationsResponseRewriteBody:       "bar_body",
+       }
+       p := NewResponseRewriteHandler()
+       out, err := p.Handle(annotations.NewExtractor(anno))
+       assert.Nil(t, err, "checking given error")
+       assert.Nil(t, out, "checking given output")
+}
+
+func TestResponseRewriteHandlerBase64(t *testing.T) {
+       anno := map[string]string{
+               annotations.AnnotationsEnableResponseRewrite:     "true",
+               annotations.AnnotationsResponseRewriteBody:       
"YmFyLWJvZHk=",
+               annotations.AnnotationsResponseRewriteBodyBase64: "true",
+       }
+       p := NewResponseRewriteHandler()
+       out, err := p.Handle(annotations.NewExtractor(anno))
+       assert.Nil(t, err, "checking given error")
+       config := out.(*adctypes.ResponseRewriteConfig)
+       assert.Equal(t, "YmFyLWJvZHk=", config.Body)
+       assert.Equal(t, true, config.BodyBase64)
+}
+
+func TestResponseRewriteHandlerInvalidStatusCode(t *testing.T) {
+       anno := map[string]string{
+               annotations.AnnotationsEnableResponseRewrite:     "true",
+               annotations.AnnotationsResponseRewriteStatusCode: "invalid",
+               annotations.AnnotationsResponseRewriteBody:       "bar_body",
+       }
+       p := NewResponseRewriteHandler()
+       out, err := p.Handle(annotations.NewExtractor(anno))
+       assert.Nil(t, err, "checking given error")
+       config := out.(*adctypes.ResponseRewriteConfig)
+       assert.Equal(t, 0, config.StatusCode, "invalid status code should 
default to 0")
+       assert.Equal(t, "bar_body", config.Body)
+}
+
+func TestParseHeaderKeyValue(t *testing.T) {
+       tests := []struct {
+               name      string
+               input     string
+               wantKey   string
+               wantValue string
+               wantFound bool
+       }{
+               {
+                       name:      "valid header",
+                       input:     "Content-Type:application/json",
+                       wantKey:   "Content-Type",
+                       wantValue: "application/json",
+                       wantFound: true,
+               },
+               {
+                       name:      "header with colon in value",
+                       input:     "X-Custom:value:with:colons",
+                       wantKey:   "X-Custom",
+                       wantValue: "value:with:colons",
+                       wantFound: true,
+               },
+               {
+                       name:      "invalid header without colon",
+                       input:     "InvalidHeader",
+                       wantKey:   "",
+                       wantValue: "",
+                       wantFound: false,
+               },
+               {
+                       name:      "empty value",
+                       input:     "X-Empty:",
+                       wantKey:   "X-Empty",
+                       wantValue: "",
+                       wantFound: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       key, value, found := parseHeaderKeyValue(tt.input)
+                       assert.Equal(t, tt.wantKey, key)
+                       assert.Equal(t, tt.wantValue, value)
+                       assert.Equal(t, tt.wantFound, found)
+               })
+       }
+}
diff --git a/internal/webhook/v1/ingress_webhook.go 
b/internal/webhook/v1/ingress_webhook.go
index 6f72451f..2099621c 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -40,13 +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/enable-response-rewrite",
-       "k8s.apisix.apache.org/response-rewrite-status-code",
-       "k8s.apisix.apache.org/response-rewrite-body",
-       "k8s.apisix.apache.org/response-rewrite-body-base64",
-       "k8s.apisix.apache.org/response-rewrite-add-header",
-       "k8s.apisix.apache.org/response-rewrite-set-header",
-       "k8s.apisix.apache.org/response-rewrite-remove-header",
        "k8s.apisix.apache.org/auth-uri",
        "k8s.apisix.apache.org/auth-ssl-verify",
        "k8s.apisix.apache.org/auth-request-headers",
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 30b8ee75..98a0bcaf 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -478,6 +478,57 @@ spec:
             name: httpbin-service-e2e-test
             port:
               number: 80
+`
+                       responseRewrite = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: response-rewrite
+  annotations:
+    k8s.apisix.apache.org/enable-response-rewrite: "true"
+    k8s.apisix.apache.org/response-rewrite-status-code: "400"
+    k8s.apisix.apache.org/response-rewrite-body: "custom response body"
+    k8s.apisix.apache.org/response-rewrite-body-base64: "false"
+    k8s.apisix.apache.org/response-rewrite-set-header: 
"X-Custom-Header:custom-value"
+    k8s.apisix.apache.org/response-rewrite-add-header: 
"X-Add-Header:added-value"
+    k8s.apisix.apache.org/response-rewrite-remove-header: "Server"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+                       responseRewriteBase64 = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: response-rewrite-base64
+  annotations:
+    k8s.apisix.apache.org/enable-response-rewrite: "true"
+    k8s.apisix.apache.org/response-rewrite-status-code: "400"
+    k8s.apisix.apache.org/response-rewrite-body: "Y3VzdG9tIHJlc3BvbnNlIGJvZHk="
+    k8s.apisix.apache.org/response-rewrite-body-base64: "true"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin-base64.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
 `
                )
                BeforeEach(func() {
@@ -843,5 +894,62 @@ spec:
                        Expect(regexUri[0]).To(Equal("/sample/(.*)"), "checking 
regex pattern")
                        Expect(regexUri[1]).To(Equal("/$1"), "checking regex 
template")
                })
+
+               It("response-rewrite", func() {
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(responseRewrite, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "httpbin.example",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusBadRequest),
+                                       
scaffold.WithExpectedBodyContains("custom response body"),
+                                       
scaffold.WithExpectedHeader("X-Custom-Header", "custom-value"),
+                                       
scaffold.WithExpectedHeader("X-Add-Header", "added-value"),
+                               },
+                       })
+
+                       By("Verify response-rewrite plugin is configured in the 
route")
+                       routes, err := 
s.DefaultDataplaneResource().Route().List(context.Background())
+                       Expect(err).NotTo(HaveOccurred(), "listing Route")
+                       Expect(routes).To(HaveLen(1), "checking Route length")
+                       
Expect(routes[0].Plugins).To(HaveKey("response-rewrite"), "checking Route 
plugins")
+
+                       jsonBytes, err := 
json.Marshal(routes[0].Plugins["response-rewrite"])
+                       Expect(err).NotTo(HaveOccurred(), "marshalling 
response-rewrite plugin config")
+                       var rewriteConfig map[string]any
+                       err = json.Unmarshal(jsonBytes, &rewriteConfig)
+                       Expect(err).NotTo(HaveOccurred(), "unmarshalling 
response-rewrite plugin config")
+                       
Expect(rewriteConfig["status_code"]).To(Equal(float64(400)), "checking status 
code")
+                       Expect(rewriteConfig["body"]).To(Equal("custom response 
body"), "checking body")
+               })
+
+               It("response-rewrite with base64", func() {
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(responseRewriteBase64, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "httpbin-base64.example",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusBadRequest),
+                                       
scaffold.WithExpectedBodyContains("custom response body"),
+                               },
+                       })
+                       By("Verify response-rewrite plugin is configured in the 
route")
+                       routes, err := 
s.DefaultDataplaneResource().Route().List(context.Background())
+                       Expect(err).NotTo(HaveOccurred(), "listing Route")
+                       Expect(routes).To(HaveLen(1), "checking Route length")
+                       
Expect(routes[0].Plugins).To(HaveKey("response-rewrite"), "checking Route 
plugins")
+
+                       jsonBytes, err := 
json.Marshal(routes[0].Plugins["response-rewrite"])
+                       Expect(err).NotTo(HaveOccurred(), "marshalling 
response-rewrite plugin config")
+                       var rewriteConfig map[string]any
+                       err = json.Unmarshal(jsonBytes, &rewriteConfig)
+                       Expect(err).NotTo(HaveOccurred(), "unmarshalling 
response-rewrite plugin config")
+                       
Expect(rewriteConfig["status_code"]).To(Equal(float64(400)), "checking status 
code")
+                       Expect(rewriteConfig["body_base64"]).To(BeTrue(), 
"checking body_base64")
+               })
        })
 })

Reply via email to