Skip to content

Commit d6cf0ea

Browse files
authored
feat: support redirect for ingress annotations (#2619)
1 parent e3c2f81 commit d6cf0ea

File tree

7 files changed

+246
-5
lines changed

7 files changed

+246
-5
lines changed

internal/adc/translator/annotations.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ import (
2121

2222
"github.com/imdario/mergo"
2323

24+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
2425
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
26+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins"
2527
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
2628
)
2729

2830
// Structure extracted by Ingress Resource
2931
type IngressConfig struct {
3032
Upstream upstream.Upstream
33+
Plugins adctypes.Plugins
3134
}
3235

33-
// parsers registered for ingress annotations
3436
var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
3537
"upstream": upstream.NewParser(),
38+
"plugins": plugins.NewParser(),
3639
}
3740

3841
func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
package plugins
16+
17+
import (
18+
logf "sigs.k8s.io/controller-runtime/pkg/log"
19+
20+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
21+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
22+
)
23+
24+
// Handler abstracts the behavior so that the apisix-ingress-controller knows
25+
// how to parse some annotations and convert them to APISIX plugins.
26+
type PluginAnnotationsHandler interface {
27+
// Handle parses the target annotation and converts it to the type-agnostic structure.
28+
// The return value might be nil since some features have an explicit switch, users should
29+
// judge whether Handle is failed by the second error value.
30+
Handle(annotations.Extractor) (any, error)
31+
// PluginName returns a string which indicates the target plugin name in APISIX.
32+
PluginName() string
33+
}
34+
35+
var (
36+
log = logf.Log.WithName("annotations").WithName("plugins").WithName("parser")
37+
38+
handlers = []PluginAnnotationsHandler{
39+
NewRedirectHandler(),
40+
}
41+
)
42+
43+
type plugins struct{}
44+
45+
func NewParser() annotations.IngressAnnotationsParser {
46+
return &plugins{}
47+
}
48+
49+
func (p *plugins) Parse(e annotations.Extractor) (any, error) {
50+
plugins := make(adctypes.Plugins)
51+
for _, handler := range handlers {
52+
out, err := handler.Handle(e)
53+
if err != nil {
54+
log.Error(err, "Failed to handle annotation", "handler", handler.PluginName())
55+
continue
56+
}
57+
if out != nil {
58+
plugins[handler.PluginName()] = out
59+
}
60+
}
61+
if len(plugins) > 0 {
62+
return plugins, nil
63+
}
64+
return nil, nil
65+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
package plugins
16+
17+
import (
18+
"net/http"
19+
"strconv"
20+
21+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
22+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
23+
)
24+
25+
type redirect struct{}
26+
27+
// NewRedirectHandler creates a handler to convert
28+
// annotations about redirect control to APISIX redirect plugin.
29+
func NewRedirectHandler() PluginAnnotationsHandler {
30+
return &redirect{}
31+
}
32+
33+
func (r *redirect) PluginName() string {
34+
return "redirect"
35+
}
36+
37+
func (r *redirect) Handle(e annotations.Extractor) (any, error) {
38+
var plugin adctypes.RedirectConfig
39+
plugin.HttpToHttps = e.GetBoolAnnotation(annotations.AnnotationsHttpToHttps)
40+
// To avoid empty redirect plugin config, adding the check about the redirect.
41+
if plugin.HttpToHttps {
42+
return &plugin, nil
43+
}
44+
if uri := e.GetStringAnnotation(annotations.AnnotationsHttpRedirect); uri != "" {
45+
// Transformation fail defaults to 0.
46+
plugin.RetCode, _ = strconv.Atoi(e.GetStringAnnotation(annotations.AnnotationsHttpRedirectCode))
47+
plugin.URI = uri
48+
// Default is http.StatusMovedPermanently, the allowed value is between http.StatusMultipleChoices and http.StatusPermanentRedirect.
49+
if plugin.RetCode < http.StatusMovedPermanently || plugin.RetCode > http.StatusPermanentRedirect {
50+
plugin.RetCode = http.StatusMovedPermanently
51+
}
52+
return &plugin, nil
53+
}
54+
return nil, nil
55+
}

internal/adc/translator/annotations_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/stretchr/testify/assert"
2323

24+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
2425
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
2526
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
2627
)
@@ -160,6 +161,34 @@ func TestTranslateIngressAnnotations(t *testing.T) {
160161
},
161162
},
162163
},
164+
{
165+
name: "redirect to https",
166+
anno: map[string]string{
167+
annotations.AnnotationsHttpToHttps: "true",
168+
},
169+
expected: &IngressConfig{
170+
Plugins: adctypes.Plugins{
171+
"redirect": &adctypes.RedirectConfig{
172+
HttpToHttps: true,
173+
},
174+
},
175+
},
176+
},
177+
{
178+
name: "redirect to specific uri",
179+
anno: map[string]string{
180+
annotations.AnnotationsHttpRedirect: "/newpath",
181+
annotations.AnnotationsHttpRedirectCode: "301",
182+
},
183+
expected: &IngressConfig{
184+
Plugins: adctypes.Plugins{
185+
"redirect": &adctypes.RedirectConfig{
186+
URI: "/newpath",
187+
RetCode: 301,
188+
},
189+
},
190+
},
191+
},
163192
}
164193

165194
for _, tt := range tests {

internal/adc/translator/ingress.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func (t *Translator) buildServiceFromIngressPath(
161161
protocol := t.resolveIngressUpstream(tctx, obj, config, path.Backend.Service, upstream)
162162
service.Upstream = upstream
163163

164-
route := buildRouteFromIngressPath(obj, path, index, labels)
164+
route := buildRouteFromIngressPath(obj, path, config, index, labels)
165165
if protocol == internaltypes.AppProtocolWS || protocol == internaltypes.AppProtocolWSS {
166166
route.EnableWebsocket = ptr.To(true)
167167
}
@@ -248,6 +248,7 @@ func (t *Translator) resolveIngressUpstream(
248248
func buildRouteFromIngressPath(
249249
obj *networkingv1.Ingress,
250250
path *networkingv1.HTTPIngressPath,
251+
config *IngressConfig,
251252
index string,
252253
labels map[string]string,
253254
) *adctypes.Route {
@@ -275,6 +276,9 @@ func buildRouteFromIngressPath(
275276
uris = []string{"/*"}
276277
}
277278
}
279+
if config != nil && len(config.Plugins) > 0 {
280+
route.Plugins = config.Plugins
281+
}
278282
route.Uris = uris
279283
return route
280284
}

internal/webhook/v1/ingress_webhook.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ var unsupportedAnnotations = []string{
4848
"k8s.apisix.apache.org/cors-allow-methods",
4949
"k8s.apisix.apache.org/enable-csrf",
5050
"k8s.apisix.apache.org/csrf-key",
51-
"k8s.apisix.apache.org/http-to-https",
52-
"k8s.apisix.apache.org/http-redirect",
53-
"k8s.apisix.apache.org/http-redirect-code",
5451
"k8s.apisix.apache.org/rewrite-target",
5552
"k8s.apisix.apache.org/rewrite-target-regex",
5653
"k8s.apisix.apache.org/rewrite-target-regex-template",

test/e2e/ingress/annotations.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,92 @@ spec:
168168
Expect(upstreams[0].Timeout.Connect).To(Equal(4), "checking Upstream connect timeout")
169169
})
170170
})
171+
172+
Context("Plugins", func() {
173+
var (
174+
tohttps = `
175+
apiVersion: networking.k8s.io/v1
176+
kind: Ingress
177+
metadata:
178+
name: tohttps
179+
annotations:
180+
k8s.apisix.apache.org/http-to-https: "true"
181+
spec:
182+
ingressClassName: %s
183+
rules:
184+
- host: httpbin.example
185+
http:
186+
paths:
187+
- path: /get
188+
pathType: Exact
189+
backend:
190+
service:
191+
name: httpbin-service-e2e-test
192+
port:
193+
number: 80
194+
`
195+
redirect = `
196+
apiVersion: networking.k8s.io/v1
197+
kind: Ingress
198+
metadata:
199+
name: redirect
200+
annotations:
201+
k8s.apisix.apache.org/http-redirect: "/anything$uri"
202+
k8s.apisix.apache.org/http-redirect-code: "308"
203+
spec:
204+
ingressClassName: %s
205+
rules:
206+
- host: httpbin.example
207+
http:
208+
paths:
209+
- path: /ip
210+
pathType: Exact
211+
backend:
212+
service:
213+
name: httpbin-service-e2e-test
214+
port:
215+
number: 80
216+
`
217+
)
218+
BeforeEach(func() {
219+
By("create GatewayProxy")
220+
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")
221+
222+
By("create IngressClass")
223+
err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
224+
Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
225+
time.Sleep(5 * time.Second)
226+
})
227+
It("redirect", func() {
228+
Expect(s.CreateResourceFromString(fmt.Sprintf(tohttps, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
229+
Expect(s.CreateResourceFromString(fmt.Sprintf(redirect, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
230+
231+
s.RequestAssert(&scaffold.RequestAssert{
232+
Method: "GET",
233+
Path: "/get",
234+
Host: "httpbin.example",
235+
Check: scaffold.WithExpectedStatus(http.StatusMovedPermanently),
236+
})
237+
s.RequestAssert(&scaffold.RequestAssert{
238+
Method: "GET",
239+
Path: "/ip",
240+
Host: "httpbin.example",
241+
Check: scaffold.WithExpectedStatus(http.StatusPermanentRedirect),
242+
})
243+
244+
_ = s.NewAPISIXClient().
245+
GET("/get").
246+
WithHost("httpbin.example").
247+
Expect().
248+
Status(http.StatusMovedPermanently).
249+
Header("Location").IsEqual("https://httpbin.example:9443/get")
250+
251+
_ = s.NewAPISIXClient().
252+
GET("/ip").
253+
WithHost("httpbin.example").
254+
Expect().
255+
Status(http.StatusPermanentRedirect).
256+
Header("Location").IsEqual("/anything/ip")
257+
})
258+
})
171259
})

0 commit comments

Comments
 (0)