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