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)
+ })
+})