Skip to content

Commit 4d2dd03

Browse files
committed
feat: support allow/block http_methods for ingress annotations
1 parent f554569 commit 4d2dd03

File tree

8 files changed

+293
-19
lines changed

8 files changed

+293
-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: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package plugins
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
10+
)
11+
12+
func TestFaultInjectionHttpAllowMethods(t *testing.T) {
13+
handler := NewFaultInjectionHandler()
14+
assert.Equal(t, "fault-injection", handler.PluginName())
15+
16+
extractor := annotations.NewExtractor(map[string]string{
17+
annotations.AnnotationsHttpAllowMethods: "GET,POST",
18+
})
19+
20+
plugin, err := handler.Handle(extractor)
21+
assert.NoError(t, err)
22+
assert.NotNil(t, plugin)
23+
24+
data, err := json.Marshal(plugin)
25+
assert.NoError(t, err)
26+
assert.JSONEq(t, `{"abort":{"http_status":405,"vars":[[["request_method","!","in",["GET","POST"]]]]}}`, string(data))
27+
}
28+
29+
func TestFaultInjectionHttpBlockMethods(t *testing.T) {
30+
handler := NewFaultInjectionHandler()
31+
assert.Equal(t, "fault-injection", handler.PluginName())
32+
33+
extractor := annotations.NewExtractor(map[string]string{
34+
annotations.AnnotationsHttpBlockMethods: "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+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var (
3838
handlers = []PluginAnnotationsHandler{
3939
NewRedirectHandler(),
4040
NewCorsHandler(),
41+
NewFaultInjectionHandler(),
4142
}
4243
)
4344

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
@@ -60,8 +60,6 @@ var unsupportedAnnotations = []string{
6060
"k8s.apisix.apache.org/auth-client-headers",
6161
"k8s.apisix.apache.org/allowlist-source-range",
6262
"k8s.apisix.apache.org/blocklist-source-range",
63-
"k8s.apisix.apache.org/http-allow-methods",
64-
"k8s.apisix.apache.org/http-block-methods",
6563
"k8s.apisix.apache.org/auth-type",
6664
"k8s.apisix.apache.org/svc-namespace",
6765
}

test/e2e/ingress/annotations.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,49 @@ spec:
317317
name: httpbin-service-e2e-test
318318
port:
319319
number: 80
320+
`
321+
allowMethods = `
322+
apiVersion: networking.k8s.io/v1
323+
kind: Ingress
324+
metadata:
325+
name: allow-methods
326+
annotations:
327+
k8s.apisix.apache.org/http-allow-methods: "GET,POST"
328+
spec:
329+
ingressClassName: %s
330+
rules:
331+
- host: httpbin.example
332+
http:
333+
paths:
334+
- path: /anything
335+
pathType: Exact
336+
backend:
337+
service:
338+
name: httpbin-service-e2e-test
339+
port:
340+
number: 80
341+
`
342+
343+
blockMethods = `
344+
apiVersion: networking.k8s.io/v1
345+
kind: Ingress
346+
metadata:
347+
name: block-methods
348+
annotations:
349+
k8s.apisix.apache.org/http-block-methods: "DELETE"
350+
spec:
351+
ingressClassName: %s
352+
rules:
353+
- host: httpbin2.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
320363
`
321364
)
322365
BeforeEach(func() {
@@ -359,5 +402,76 @@ spec:
359402
Status(http.StatusPermanentRedirect).
360403
Header("Location").IsEqual("/anything/ip")
361404
})
405+
It("methods", func() {
406+
Expect(s.CreateResourceFromString(fmt.Sprintf(allowMethods, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
407+
Expect(s.CreateResourceFromString(fmt.Sprintf(blockMethods, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
408+
409+
tets := []*scaffold.RequestAssert{
410+
{
411+
Method: "GET",
412+
Path: "/anything",
413+
Host: "httpbin.example",
414+
Check: scaffold.WithExpectedStatus(http.StatusOK),
415+
},
416+
{
417+
Method: "POST",
418+
Path: "/anything",
419+
Host: "httpbin.example",
420+
Check: scaffold.WithExpectedStatus(http.StatusOK),
421+
},
422+
{
423+
Method: "PUT",
424+
Path: "/anything",
425+
Host: "httpbin.example",
426+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
427+
},
428+
{
429+
Method: "PATCH",
430+
Path: "/anything",
431+
Host: "httpbin.example",
432+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
433+
},
434+
{
435+
Method: "DELETE",
436+
Path: "/anything",
437+
Host: "httpbin.example",
438+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
439+
},
440+
{
441+
Method: "GET",
442+
Path: "/anything",
443+
Host: "httpbin2.example",
444+
Check: scaffold.WithExpectedStatus(http.StatusOK),
445+
},
446+
{
447+
Method: "POST",
448+
Path: "/anything",
449+
Host: "httpbin2.example",
450+
Check: scaffold.WithExpectedStatus(http.StatusOK),
451+
},
452+
{
453+
Method: "PUT",
454+
Path: "/anything",
455+
Host: "httpbin2.example",
456+
Check: scaffold.WithExpectedStatus(http.StatusOK),
457+
},
458+
{
459+
Method: "PATCH",
460+
Path: "/anything",
461+
Host: "httpbin2.example",
462+
Check: scaffold.WithExpectedStatus(http.StatusOK),
463+
},
464+
{
465+
Method: "DELETE",
466+
Path: "/anything",
467+
Host: "httpbin2.example",
468+
Check: scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
469+
},
470+
}
471+
472+
for _, test := range tets {
473+
s.RequestAssert(test)
474+
}
475+
})
362476
})
363477
})

test/e2e/scaffold/assertion.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package scaffold
1919

2020
import (
21+
"encoding/json"
2122
"fmt"
2223
"io"
2324
"net"
@@ -62,25 +63,26 @@ type HTTPResponse struct {
6263
}
6364

6465
type BasicAuth struct {
65-
Username string
66-
Password string
66+
Username string `json:"username"`
67+
Password string `json:"password"`
6768
}
6869

6970
type RequestAssert struct {
70-
Client *httpexpect.Expect
71-
Method string
72-
Path string
73-
Host string
74-
Query map[string]any
75-
Headers map[string]string
76-
Body []byte
77-
BasicAuth *BasicAuth
78-
79-
Timeout time.Duration
80-
Interval time.Duration
81-
82-
Check ResponseCheckFunc
83-
Checks []ResponseCheckFunc
71+
Method string `json:"method,omitempty"`
72+
Path string `json:"path,omitempty"`
73+
Host string `json:"host,omitempty"`
74+
Query map[string]any `json:"query,omitempty"`
75+
Headers map[string]string `json:"headers,omitempty"`
76+
Body []byte `json:"body,omitempty"`
77+
BasicAuth *BasicAuth `json:"basic_auth,omitempty"`
78+
79+
Client *httpexpect.Expect `json:"-"`
80+
81+
Timeout time.Duration `json:"-"`
82+
Interval time.Duration `json:"-"`
83+
84+
Check ResponseCheckFunc `json:"-"`
85+
Checks []ResponseCheckFunc `json:"-"`
8486
}
8587

8688
func (c *RequestAssert) request(method, path string, body []byte) *httpexpect.Request {
@@ -308,7 +310,8 @@ func (s *Scaffold) RequestAssert(r *RequestAssert) bool {
308310

309311
for _, check := range r.Checks {
310312
if err := check(resp); err != nil {
311-
return fmt.Errorf("response check failed: %w", err)
313+
req, _ := json.MarshalIndent(r, "", " ")
314+
return fmt.Errorf("response check failed for request %s: %v", string(req), err)
312315
}
313316
}
314317
return nil

0 commit comments

Comments
 (0)