Skip to content

Commit ee561f7

Browse files
authored
feat: support allow/block http_methods for ingress annotations (#2623)
1 parent 36a3196 commit ee561f7

File tree

8 files changed

+310
-19
lines changed

8 files changed

+310
-19
lines changed

api/adc/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,15 @@ type ResponseRewriteConfig struct {
659659
Filters []map[string]string `json:"filters,omitempty" yaml:"filters,omitempty"`
660660
}
661661

662+
type FaultInjectionConfig struct {
663+
Abort *FaultInjectionAbortConfig `json:"abort,omitempty" yaml:"abort,omitempty"`
664+
}
665+
666+
type FaultInjectionAbortConfig struct {
667+
HTTPStatus int `json:"http_status" yaml:"http_status"`
668+
Vars [][]expr.Expr `json:"vars,omitempty" yaml:"vars,omitempty"`
669+
}
670+
662671
type ResponseHeaders struct {
663672
Set map[string]string `json:"set,omitempty" yaml:"set,omitempty"`
664673
Add []string `json:"add,omitempty" yaml:"add,omitempty"`
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
16+
package plugins
17+
18+
import (
19+
"net/http"
20+
21+
"github.com/incubator4/go-resty-expr/expr"
22+
23+
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
type FaultInjection struct{}
28+
29+
// FaultInjection to APISIX fault-injection plugin.
30+
func NewFaultInjectionHandler() PluginAnnotationsHandler {
31+
return &FaultInjection{}
32+
}
33+
34+
func (h FaultInjection) PluginName() string {
35+
return "fault-injection"
36+
}
37+
38+
func (f FaultInjection) Handle(e annotations.Extractor) (any, error) {
39+
var plugin adctypes.FaultInjectionConfig
40+
41+
allowMethods := e.GetStringsAnnotation(annotations.AnnotationsHttpAllowMethods)
42+
blockMethods := e.GetStringsAnnotation(annotations.AnnotationsHttpBlockMethods)
43+
if len(allowMethods) == 0 && len(blockMethods) == 0 {
44+
return nil, nil
45+
}
46+
abort := &adctypes.FaultInjectionAbortConfig{
47+
HTTPStatus: http.StatusMethodNotAllowed,
48+
}
49+
if len(allowMethods) > 0 {
50+
abort.Vars = [][]expr.Expr{{
51+
expr.StringExpr("request_method").Not().In(
52+
expr.ArrayExpr(expr.ExprArrayFromStrings(allowMethods)...),
53+
),
54+
}}
55+
} else {
56+
abort.Vars = [][]expr.Expr{{
57+
expr.StringExpr("request_method").In(
58+
expr.ArrayExpr(expr.ExprArrayFromStrings(blockMethods)...),
59+
),
60+
}}
61+
}
62+
plugin.Abort = abort
63+
return &plugin, nil
64+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package plugins
19+
20+
import (
21+
"encoding/json"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
26+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
27+
)
28+
29+
func TestFaultInjectionHttpAllowMethods(t *testing.T) {
30+
handler := NewFaultInjectionHandler()
31+
assert.Equal(t, "fault-injection", handler.PluginName())
32+
33+
extractor := annotations.NewExtractor(map[string]string{
34+
annotations.AnnotationsHttpAllowMethods: "GET,POST",
35+
})
36+
37+
plugin, err := handler.Handle(extractor)
38+
assert.NoError(t, err)
39+
assert.NotNil(t, plugin)
40+
41+
data, err := json.Marshal(plugin)
42+
assert.NoError(t, err)
43+
assert.JSONEq(t, `{"abort":{"http_status":405,"vars":[[["request_method","!","in",["GET","POST"]]]]}}`, string(data))
44+
}
45+
46+
func TestFaultInjectionHttpBlockMethods(t *testing.T) {
47+
handler := NewFaultInjectionHandler()
48+
assert.Equal(t, "fault-injection", handler.PluginName())
49+
50+
extractor := annotations.NewExtractor(map[string]string{
51+
annotations.AnnotationsHttpBlockMethods: "GET,POST",
52+
})
53+
54+
plugin, err := handler.Handle(extractor)
55+
assert.NoError(t, err)
56+
assert.NotNil(t, plugin)
57+
58+
data, err := json.Marshal(plugin)
59+
assert.NoError(t, err)
60+
assert.JSONEq(t, `{"abort":{"http_status":405,"vars":[[["request_method","in",["GET","POST"]]]]}}`, string(data))
61+
}

internal/adc/translator/annotations/plugins/plugins.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
NewRedirectHandler(),
4040
NewCorsHandler(),
4141
NewCSRFHandler(),
42+
NewFaultInjectionHandler(),
4243
}
4344
)
4445

internal/adc/translator/annotations_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"testing"
2121

22+
"github.com/incubator4/go-resty-expr/expr"
2223
"github.com/stretchr/testify/assert"
2324

2425
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
@@ -216,6 +217,46 @@ func TestTranslateIngressAnnotations(t *testing.T) {
216217
EnableWebsocket: true,
217218
},
218219
},
220+
{
221+
name: "fault injection by allowed http methods",
222+
anno: map[string]string{
223+
annotations.AnnotationsHttpAllowMethods: "GET,POST",
224+
},
225+
expected: &IngressConfig{
226+
Plugins: adctypes.Plugins{
227+
"fault-injection": &adctypes.FaultInjectionConfig{
228+
Abort: &adctypes.FaultInjectionAbortConfig{
229+
HTTPStatus: 405,
230+
Vars: [][]expr.Expr{{
231+
expr.StringExpr("request_method").Not().In(
232+
expr.ArrayExpr(expr.ExprArrayFromStrings([]string{"GET", "POST"})...),
233+
),
234+
}},
235+
},
236+
},
237+
},
238+
},
239+
},
240+
{
241+
name: "fault injection by blocked http methods",
242+
anno: map[string]string{
243+
annotations.AnnotationsHttpBlockMethods: "DELETE",
244+
},
245+
expected: &IngressConfig{
246+
Plugins: adctypes.Plugins{
247+
"fault-injection": &adctypes.FaultInjectionConfig{
248+
Abort: &adctypes.FaultInjectionAbortConfig{
249+
HTTPStatus: 405,
250+
Vars: [][]expr.Expr{{
251+
expr.StringExpr("request_method").In(
252+
expr.ArrayExpr(expr.ExprArrayFromStrings([]string{"DELETE"})...),
253+
),
254+
}},
255+
},
256+
},
257+
},
258+
},
259+
},
219260
}
220261

221262
for _, tt := range tests {

internal/webhook/v1/ingress_webhook.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ var unsupportedAnnotations = []string{
5757
"k8s.apisix.apache.org/auth-client-headers",
5858
"k8s.apisix.apache.org/allowlist-source-range",
5959
"k8s.apisix.apache.org/blocklist-source-range",
60-
"k8s.apisix.apache.org/http-allow-methods",
61-
"k8s.apisix.apache.org/http-block-methods",
6260
"k8s.apisix.apache.org/auth-type",
6361
"k8s.apisix.apache.org/svc-namespace",
6462
}

test/e2e/ingress/annotations.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,49 @@ spec:
339339
name: httpbin-service-e2e-test
340340
port:
341341
number: 80
342+
`
343+
allowMethods = `
344+
apiVersion: networking.k8s.io/v1
345+
kind: Ingress
346+
metadata:
347+
name: allow-methods
348+
annotations:
349+
k8s.apisix.apache.org/http-allow-methods: "GET,POST"
350+
spec:
351+
ingressClassName: %s
352+
rules:
353+
- host: httpbin.example
354+
http:
355+
paths:
356+
- path: /anything
357+
pathType: Exact
358+
backend:
359+
service:
360+
name: httpbin-service-e2e-test
361+
port:
362+
number: 80
363+
`
364+
365+
blockMethods = `
366+
apiVersion: networking.k8s.io/v1
367+
kind: Ingress
368+
metadata:
369+
name: block-methods
370+
annotations:
371+
k8s.apisix.apache.org/http-block-methods: "DELETE"
372+
spec:
373+
ingressClassName: %s
374+
rules:
375+
- host: httpbin2.example
376+
http:
377+
paths:
378+
- path: /anything
379+
pathType: Exact
380+
backend:
381+
service:
382+
name: httpbin-service-e2e-test
383+
port:
384+
number: 80
342385
`
343386
)
344387
BeforeEach(func() {
@@ -496,5 +539,76 @@ spec:
496539
Expect(err).NotTo(HaveOccurred(), "unmarshalling echo plugin config")
497540
Expect(echoConfig["body"]).To(Equal("hello from plugin config"), "checking echo plugin body")
498541
})
542+
It("methods", func() {
543+
Expect(s.CreateResourceFromString(fmt.Sprintf(allowMethods, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
544+
Expect(s.CreateResourceFromString(fmt.Sprintf(blockMethods, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
545+
546+
tets := []*scaffold.RequestAssert{
547+
{
548+
Method: "GET",
549+
Path: "/anything",
550+
Host: "httpbin.example",
551+
Check: scaffold.WithExpectedStatus(http.StatusOK),
552+
},
553+
{
554+
Method: "POST",
555+
Path: "/anything",
556+
Host: "httpbin.example",
557+
Check: scaffold.WithExpectedStatus(http.StatusOK),
558+
},
559+
{
560+
Method: "PUT",
561+
Path: "/anything",
562+
Host: "httpbin.example",
563+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
564+
},
565+
{
566+
Method: "PATCH",
567+
Path: "/anything",
568+
Host: "httpbin.example",
569+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
570+
},
571+
{
572+
Method: "DELETE",
573+
Path: "/anything",
574+
Host: "httpbin.example",
575+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
576+
},
577+
{
578+
Method: "GET",
579+
Path: "/anything",
580+
Host: "httpbin2.example",
581+
Check: scaffold.WithExpectedStatus(http.StatusOK),
582+
},
583+
{
584+
Method: "POST",
585+
Path: "/anything",
586+
Host: "httpbin2.example",
587+
Check: scaffold.WithExpectedStatus(http.StatusOK),
588+
},
589+
{
590+
Method: "PUT",
591+
Path: "/anything",
592+
Host: "httpbin2.example",
593+
Check: scaffold.WithExpectedStatus(http.StatusOK),
594+
},
595+
{
596+
Method: "PATCH",
597+
Path: "/anything",
598+
Host: "httpbin2.example",
599+
Check: scaffold.WithExpectedStatus(http.StatusOK),
600+
},
601+
{
602+
Method: "DELETE",
603+
Path: "/anything",
604+
Host: "httpbin2.example",
605+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
606+
},
607+
}
608+
609+
for _, test := range tets {
610+
s.RequestAssert(test)
611+
}
612+
})
499613
})
500614
})

0 commit comments

Comments
 (0)