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

alinsran 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 d6cf0eaa feat: support redirect for ingress annotations (#2619)
d6cf0eaa is described below

commit d6cf0eaaad1858e89f95fff9300c13a241ab270a
Author: AlinsRan <[email protected]>
AuthorDate: Mon Oct 27 12:26:30 2025 +0800

    feat: support redirect for ingress annotations (#2619)
---
 internal/adc/translator/annotations.go             |  5 +-
 .../adc/translator/annotations/plugins/plugins.go  | 65 ++++++++++++++++
 .../adc/translator/annotations/plugins/redirect.go | 55 ++++++++++++++
 internal/adc/translator/annotations_test.go        | 29 +++++++
 internal/adc/translator/ingress.go                 |  6 +-
 internal/webhook/v1/ingress_webhook.go             |  3 -
 test/e2e/ingress/annotations.go                    | 88 ++++++++++++++++++++++
 7 files changed, 246 insertions(+), 5 deletions(-)

diff --git a/internal/adc/translator/annotations.go 
b/internal/adc/translator/annotations.go
index 28319b65..3f394216 100644
--- a/internal/adc/translator/annotations.go
+++ b/internal/adc/translator/annotations.go
@@ -21,18 +21,21 @@ import (
 
        "github.com/imdario/mergo"
 
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 )
 
 // Structure extracted by Ingress Resource
 type IngressConfig struct {
        Upstream upstream.Upstream
+       Plugins  adctypes.Plugins
 }
 
-// parsers registered for ingress annotations
 var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
        "upstream": upstream.NewParser(),
+       "plugins":  plugins.NewParser(),
 }
 
 func (t *Translator) TranslateIngressAnnotations(anno map[string]string) 
*IngressConfig {
diff --git a/internal/adc/translator/annotations/plugins/plugins.go 
b/internal/adc/translator/annotations/plugins/plugins.go
new file mode 100644
index 00000000..5243e27b
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/plugins.go
@@ -0,0 +1,65 @@
+// 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 (
+       logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+// Handler abstracts the behavior so that the apisix-ingress-controller knows
+// how to parse some annotations and convert them to APISIX plugins.
+type PluginAnnotationsHandler interface {
+       // Handle parses the target annotation and converts it to the 
type-agnostic structure.
+       // The return value might be nil since some features have an explicit 
switch, users should
+       // judge whether Handle is failed by the second error value.
+       Handle(annotations.Extractor) (any, error)
+       // PluginName returns a string which indicates the target plugin name 
in APISIX.
+       PluginName() string
+}
+
+var (
+       log = 
logf.Log.WithName("annotations").WithName("plugins").WithName("parser")
+
+       handlers = []PluginAnnotationsHandler{
+               NewRedirectHandler(),
+       }
+)
+
+type plugins struct{}
+
+func NewParser() annotations.IngressAnnotationsParser {
+       return &plugins{}
+}
+
+func (p *plugins) Parse(e annotations.Extractor) (any, error) {
+       plugins := make(adctypes.Plugins)
+       for _, handler := range handlers {
+               out, err := handler.Handle(e)
+               if err != nil {
+                       log.Error(err, "Failed to handle annotation", 
"handler", handler.PluginName())
+                       continue
+               }
+               if out != nil {
+                       plugins[handler.PluginName()] = out
+               }
+       }
+       if len(plugins) > 0 {
+               return plugins, nil
+       }
+       return nil, nil
+}
diff --git a/internal/adc/translator/annotations/plugins/redirect.go 
b/internal/adc/translator/annotations/plugins/redirect.go
new file mode 100644
index 00000000..b26544fe
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/redirect.go
@@ -0,0 +1,55 @@
+// 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 (
+       "net/http"
+       "strconv"
+
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+type redirect struct{}
+
+// NewRedirectHandler creates a handler to convert
+// annotations about redirect control to APISIX redirect plugin.
+func NewRedirectHandler() PluginAnnotationsHandler {
+       return &redirect{}
+}
+
+func (r *redirect) PluginName() string {
+       return "redirect"
+}
+
+func (r *redirect) Handle(e annotations.Extractor) (any, error) {
+       var plugin adctypes.RedirectConfig
+       plugin.HttpToHttps = 
e.GetBoolAnnotation(annotations.AnnotationsHttpToHttps)
+       // To avoid empty redirect plugin config, adding the check about the 
redirect.
+       if plugin.HttpToHttps {
+               return &plugin, nil
+       }
+       if uri := e.GetStringAnnotation(annotations.AnnotationsHttpRedirect); 
uri != "" {
+               // Transformation fail defaults to 0.
+               plugin.RetCode, _ = 
strconv.Atoi(e.GetStringAnnotation(annotations.AnnotationsHttpRedirectCode))
+               plugin.URI = uri
+               // Default is http.StatusMovedPermanently, the allowed value is 
between http.StatusMultipleChoices and http.StatusPermanentRedirect.
+               if plugin.RetCode < http.StatusMovedPermanently || 
plugin.RetCode > http.StatusPermanentRedirect {
+                       plugin.RetCode = http.StatusMovedPermanently
+               }
+               return &plugin, nil
+       }
+       return nil, nil
+}
diff --git a/internal/adc/translator/annotations_test.go 
b/internal/adc/translator/annotations_test.go
index 8216be3f..2e43e035 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -21,6 +21,7 @@ import (
 
        "github.com/stretchr/testify/assert"
 
+       adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 )
@@ -160,6 +161,34 @@ func TestTranslateIngressAnnotations(t *testing.T) {
                                },
                        },
                },
+               {
+                       name: "redirect to https",
+                       anno: map[string]string{
+                               annotations.AnnotationsHttpToHttps: "true",
+                       },
+                       expected: &IngressConfig{
+                               Plugins: adctypes.Plugins{
+                                       "redirect": &adctypes.RedirectConfig{
+                                               HttpToHttps: true,
+                                       },
+                               },
+                       },
+               },
+               {
+                       name: "redirect to specific uri",
+                       anno: map[string]string{
+                               annotations.AnnotationsHttpRedirect:     
"/newpath",
+                               annotations.AnnotationsHttpRedirectCode: "301",
+                       },
+                       expected: &IngressConfig{
+                               Plugins: adctypes.Plugins{
+                                       "redirect": &adctypes.RedirectConfig{
+                                               URI:     "/newpath",
+                                               RetCode: 301,
+                                       },
+                               },
+                       },
+               },
        }
 
        for _, tt := range tests {
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index da3c5d0e..bf570738 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -161,7 +161,7 @@ func (t *Translator) buildServiceFromIngressPath(
        protocol := t.resolveIngressUpstream(tctx, obj, config, 
path.Backend.Service, upstream)
        service.Upstream = upstream
 
-       route := buildRouteFromIngressPath(obj, path, index, labels)
+       route := buildRouteFromIngressPath(obj, path, config, index, labels)
        if protocol == internaltypes.AppProtocolWS || protocol == 
internaltypes.AppProtocolWSS {
                route.EnableWebsocket = ptr.To(true)
        }
@@ -248,6 +248,7 @@ func (t *Translator) resolveIngressUpstream(
 func buildRouteFromIngressPath(
        obj *networkingv1.Ingress,
        path *networkingv1.HTTPIngressPath,
+       config *IngressConfig,
        index string,
        labels map[string]string,
 ) *adctypes.Route {
@@ -275,6 +276,9 @@ func buildRouteFromIngressPath(
                        uris = []string{"/*"}
                }
        }
+       if config != nil && len(config.Plugins) > 0 {
+               route.Plugins = config.Plugins
+       }
        route.Uris = uris
        return route
 }
diff --git a/internal/webhook/v1/ingress_webhook.go 
b/internal/webhook/v1/ingress_webhook.go
index 4941a821..c3c78d2d 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -48,9 +48,6 @@ var unsupportedAnnotations = []string{
        "k8s.apisix.apache.org/cors-allow-methods",
        "k8s.apisix.apache.org/enable-csrf",
        "k8s.apisix.apache.org/csrf-key",
-       "k8s.apisix.apache.org/http-to-https",
-       "k8s.apisix.apache.org/http-redirect",
-       "k8s.apisix.apache.org/http-redirect-code",
        "k8s.apisix.apache.org/rewrite-target",
        "k8s.apisix.apache.org/rewrite-target-regex",
        "k8s.apisix.apache.org/rewrite-target-regex-template",
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 3043d5b1..0f3079e3 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -168,4 +168,92 @@ spec:
                        Expect(upstreams[0].Timeout.Connect).To(Equal(4), 
"checking Upstream connect timeout")
                })
        })
+
+       Context("Plugins", func() {
+               var (
+                       tohttps = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: tohttps
+  annotations:
+    k8s.apisix.apache.org/http-to-https: "true"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+                       redirect = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: redirect
+  annotations:
+    k8s.apisix.apache.org/http-redirect: "/anything$uri"
+    k8s.apisix.apache.org/http-redirect-code: "308"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin.example
+    http:
+      paths:
+      - path: /ip
+        pathType: Exact
+        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("redirect", func() {
+                       Expect(s.CreateResourceFromString(fmt.Sprintf(tohttps, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+                       Expect(s.CreateResourceFromString(fmt.Sprintf(redirect, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "httpbin.example",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusMovedPermanently),
+                       })
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/ip",
+                               Host:   "httpbin.example",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusPermanentRedirect),
+                       })
+
+                       _ = s.NewAPISIXClient().
+                               GET("/get").
+                               WithHost("httpbin.example").
+                               Expect().
+                               Status(http.StatusMovedPermanently).
+                               
Header("Location").IsEqual("https://httpbin.example:9443/get";)
+
+                       _ = s.NewAPISIXClient().
+                               GET("/ip").
+                               WithHost("httpbin.example").
+                               Expect().
+                               Status(http.StatusPermanentRedirect).
+                               Header("Location").IsEqual("/anything/ip")
+               })
+       })
 })

Reply via email to