feat: support rewrite annotation (#480)

diff --git a/docs/en/latest/concepts/annotations.md b/docs/en/latest/concepts/annotations.md
index 01b8774..9b8094c 100644
--- a/docs/en/latest/concepts/annotations.md
+++ b/docs/en/latest/concepts/annotations.md
@@ -59,3 +59,41 @@
 
 You can specify the denied client IP addresses or nets by the annotation `k8s.apisix.apache.org/blocklist-source-range`, multiple IP addresses or nets join together with `,`,
 for instance, `k8s.apisix.apache.org/blocklist-source-range: 127.0.0.1,172.17.0.0/16`. Default value is *empty*, which means the sources are not limited.
+
+Rewrite Target
+--------------
+
+You can rewrite requests by specifying the annotation `k8s.apisix.apache.org/rewrite-target` or `k8s.apisix.apache.org/rewrite-target-regex`.
+
+The annotation `k8s.apisix.apache.org/rewrite-target` controls where the request will be forwarded to.
+
+If you want to use regex and match groups, use annotation `k8s.apisix.apache.org/rewrite-target-regex` and `k8s.apisix.apache.org/rewrite-target-regex-template`. The first annotation contains the matching rule (regex), the second one contains the rewrite rule.
+
+Both annotations must be used together, otherwise they will be ignored.
+
+For example, we have an Ingress matches prefix path `/app`, and we set `k8s.apisix.apache.org/rewrite-target-regex` to `/app/(.*)` and set `k8s.apisix.apache.org/rewrite-target-regex-template` to `/$1`.
+
+```yaml
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target-regex: "/app/(.*)"
+    k8s.apisix.apache.org/rewrite-target-regex-template: "/$1"
+  name: ingress-v1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /app
+        pathType: Prefix
+        backend:
+          service:
+            name: httpbin
+            port:
+              number: 80
+```
+
+With this Ingress, any requests with `/app` prefix will be forwarded to backend without the `/app/` part, e.g. request `/app/ip` will be forwarded to `/ip`.  
diff --git a/pkg/kube/translation/annotations.go b/pkg/kube/translation/annotations.go
index 6d515ee..23ff8cb 100644
--- a/pkg/kube/translation/annotations.go
+++ b/pkg/kube/translation/annotations.go
@@ -26,6 +26,7 @@
 	_handlers = []annotations.Handler{
 		annotations.NewCorsHandler(),
 		annotations.NewIPRestrictionHandler(),
+		annotations.NewRewriteHandler(),
 	}
 )
 
diff --git a/pkg/kube/translation/annotations/rewrite.go b/pkg/kube/translation/annotations/rewrite.go
new file mode 100644
index 0000000..72e2385
--- /dev/null
+++ b/pkg/kube/translation/annotations/rewrite.go
@@ -0,0 +1,58 @@
+// 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 annotations
+
+import (
+	"regexp"
+
+	apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
+)
+
+const (
+	_rewriteTarget              = "k8s.apisix.apache.org/rewrite-target"
+	_rewriteTargetRegex         = "k8s.apisix.apache.org/rewrite-target-regex"
+	_rewriteTargetRegexTemplate = "k8s.apisix.apache.org/rewrite-target-regex-template"
+)
+
+type rewrite struct{}
+
+// NewRewriteHandler creates a handler to convert
+// annotations about request rewrite control to APISIX proxy-rewrite plugin.
+func NewRewriteHandler() Handler {
+	return &rewrite{}
+}
+
+func (i *rewrite) PluginName() string {
+	return "proxy-rewrite"
+}
+
+func (i *rewrite) Handle(e Extractor) (interface{}, error) {
+	var plugin apisixv1.RewriteConfig
+	rewriteTarget := e.GetStringAnnotation(_rewriteTarget)
+	rewriteTargetRegex := e.GetStringAnnotation(_rewriteTargetRegex)
+	rewriteTemplate := e.GetStringAnnotation(_rewriteTargetRegexTemplate)
+	if rewriteTarget != "" || rewriteTargetRegex != "" || rewriteTemplate != "" {
+		plugin.RewriteTarget = rewriteTarget
+		if rewriteTargetRegex != "" && rewriteTemplate != "" {
+			_, err := regexp.Compile(rewriteTargetRegex)
+			if err != nil {
+				return nil, err
+			}
+			plugin.RewriteTargetRegex = []string{rewriteTargetRegex, rewriteTemplate}
+		}
+		return &plugin, nil
+	}
+	return nil, nil
+}
diff --git a/pkg/types/apisix/v1/plugin_types.go b/pkg/types/apisix/v1/plugin_types.go
index 07782e6..a11c820 100644
--- a/pkg/types/apisix/v1/plugin_types.go
+++ b/pkg/types/apisix/v1/plugin_types.go
@@ -48,3 +48,10 @@
 	AllowMethods string `json:"allow_methods,omitempty"`
 	AllowHeaders string `json:"allow_headers,omitempty"`
 }
+
+// RewriteConfig is the rule config for proxy-rewrite plugin.
+// +k8s:deepcopy-gen=true
+type RewriteConfig struct {
+	RewriteTarget      string   `json:"uri,omitempty"`
+	RewriteTargetRegex []string `json:"regex_uri,omitempty"`
+}
diff --git a/pkg/types/apisix/v1/zz_generated.deepcopy.go b/pkg/types/apisix/v1/zz_generated.deepcopy.go
index 249f427..1025ab1 100644
--- a/pkg/types/apisix/v1/zz_generated.deepcopy.go
+++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go
@@ -127,6 +127,27 @@
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RewriteConfig) DeepCopyInto(out *RewriteConfig) {
+	*out = *in
+	if in.RewriteTargetRegex != nil {
+		in, out := &in.RewriteTargetRegex, &out.RewriteTargetRegex
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteConfig.
+func (in *RewriteConfig) DeepCopy() *RewriteConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(RewriteConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Route) DeepCopyInto(out *Route) {
 	*out = *in
 	in.Metadata.DeepCopyInto(&out.Metadata)
diff --git a/test/e2e/annotations/rewrite.go b/test/e2e/annotations/rewrite.go
new file mode 100644
index 0000000..581f4a9
--- /dev/null
+++ b/test/e2e/annotations/rewrite.go
@@ -0,0 +1,215 @@
+// 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 annotations
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/onsi/ginkgo"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = ginkgo.Describe("rewrite annotations", func() {
+	s := scaffold.NewDefaultScaffold()
+
+	ginkgo.It("enable in ingress networking/v1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target: "/ip"
+  name: ingress-v1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Exact
+        backend:
+          service:
+            name: %s
+            port:
+              number: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound)
+	})
+
+	ginkgo.It("enable in ingress networking/v1beta1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1beta1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target: "/ip"
+  name: ingress-v1beta1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Exact
+        backend:
+          serviceName: %s
+          servicePort: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound)
+	})
+
+	ginkgo.It("enable in ingress extensions/v1beta1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target: "/ip"
+  name: ingress-extensions-v1beta1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Exact
+        backend:
+          serviceName: %s
+          servicePort: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusNotFound)
+	})
+})
+
+var _ = ginkgo.Describe("rewrite regex annotations", func() {
+	s := scaffold.NewDefaultScaffold()
+
+	ginkgo.It("enable in ingress networking/v1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target-regex: "/sample/(.*)"
+    k8s.apisix.apache.org/rewrite-target-regex-template: "/$1"
+  name: ingress-v1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Prefix
+        backend:
+          service:
+            name: %s
+            port:
+              number: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/sample/get").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+	})
+
+	ginkgo.It("enable in ingress networking/v1beta1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1beta1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target-regex: "/sample/(.*)"
+    k8s.apisix.apache.org/rewrite-target-regex-template: "/$1"
+  name: ingress-v1beta1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Prefix
+        backend:
+          serviceName: %s
+          servicePort: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/sample/get").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+	})
+
+	ginkgo.It("enable in ingress extensions/v1beta1", func() {
+		backendSvc, backendPort := s.DefaultHTTPBackend()
+		ing := fmt.Sprintf(`
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  annotations:
+    kubernetes.io/ingress.class: apisix
+    k8s.apisix.apache.org/rewrite-target-regex: "/sample/(.*)"
+    k8s.apisix.apache.org/rewrite-target-regex-template: "/$1"
+  name: ingress-extensions-v1beta1
+spec:
+  rules:
+  - host: httpbin.org
+    http:
+      paths:
+      - path: /sample
+        pathType: Prefix
+        backend:
+          serviceName: %s
+          servicePort: %d
+`, backendSvc, backendPort[0])
+		err := s.CreateResourceFromString(ing)
+		assert.Nil(ginkgo.GinkgoT(), err, "creating ingress")
+		time.Sleep(5 * time.Second)
+
+		_ = s.NewAPISIXClient().GET("/sample/ip").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+		_ = s.NewAPISIXClient().GET("/sample/get").WithHeader("Host", "httpbin.org").Expect().Status(http.StatusOK)
+	})
+})