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 ee561f73 feat: support allow/block http_methods for ingress
annotations (#2623)
ee561f73 is described below
commit ee561f73d1ed9723a86ca8d9c9682b9f38b05a20
Author: AlinsRan <[email protected]>
AuthorDate: Wed Oct 29 13:56:59 2025 +0800
feat: support allow/block http_methods for ingress annotations (#2623)
---
api/adc/types.go | 9 ++
.../annotations/plugins/fault-injection.go | 64 ++++++++++++
.../annotations/plugins/fault_injection_test.go | 61 +++++++++++
.../adc/translator/annotations/plugins/plugins.go | 1 +
internal/adc/translator/annotations_test.go | 41 ++++++++
internal/webhook/v1/ingress_webhook.go | 2 -
test/e2e/ingress/annotations.go | 114 +++++++++++++++++++++
test/e2e/scaffold/assertion.go | 37 ++++---
8 files changed, 310 insertions(+), 19 deletions(-)
diff --git a/api/adc/types.go b/api/adc/types.go
index 8c2a9506..b18e6fdb 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -659,6 +659,15 @@ type ResponseRewriteConfig struct {
Filters []map[string]string `json:"filters,omitempty"
yaml:"filters,omitempty"`
}
+type FaultInjectionConfig struct {
+ Abort *FaultInjectionAbortConfig `json:"abort,omitempty"
yaml:"abort,omitempty"`
+}
+
+type FaultInjectionAbortConfig struct {
+ HTTPStatus int `json:"http_status" yaml:"http_status"`
+ Vars [][]expr.Expr `json:"vars,omitempty" yaml:"vars,omitempty"`
+}
+
type ResponseHeaders struct {
Set map[string]string `json:"set,omitempty" yaml:"set,omitempty"`
Add []string `json:"add,omitempty" yaml:"add,omitempty"`
diff --git a/internal/adc/translator/annotations/plugins/fault-injection.go
b/internal/adc/translator/annotations/plugins/fault-injection.go
new file mode 100644
index 00000000..9e5ad09b
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/fault-injection.go
@@ -0,0 +1,64 @@
+// 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"
+
+ "github.com/incubator4/go-resty-expr/expr"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+type FaultInjection struct{}
+
+// FaultInjection to APISIX fault-injection plugin.
+func NewFaultInjectionHandler() PluginAnnotationsHandler {
+ return &FaultInjection{}
+}
+
+func (h FaultInjection) PluginName() string {
+ return "fault-injection"
+}
+
+func (f FaultInjection) Handle(e annotations.Extractor) (any, error) {
+ var plugin adctypes.FaultInjectionConfig
+
+ allowMethods :=
e.GetStringsAnnotation(annotations.AnnotationsHttpAllowMethods)
+ blockMethods :=
e.GetStringsAnnotation(annotations.AnnotationsHttpBlockMethods)
+ if len(allowMethods) == 0 && len(blockMethods) == 0 {
+ return nil, nil
+ }
+ abort := &adctypes.FaultInjectionAbortConfig{
+ HTTPStatus: http.StatusMethodNotAllowed,
+ }
+ if len(allowMethods) > 0 {
+ abort.Vars = [][]expr.Expr{{
+ expr.StringExpr("request_method").Not().In(
+
expr.ArrayExpr(expr.ExprArrayFromStrings(allowMethods)...),
+ ),
+ }}
+ } else {
+ abort.Vars = [][]expr.Expr{{
+ expr.StringExpr("request_method").In(
+
expr.ArrayExpr(expr.ExprArrayFromStrings(blockMethods)...),
+ ),
+ }}
+ }
+ plugin.Abort = abort
+ return &plugin, nil
+}
diff --git
a/internal/adc/translator/annotations/plugins/fault_injection_test.go
b/internal/adc/translator/annotations/plugins/fault_injection_test.go
new file mode 100644
index 00000000..4c7ab615
--- /dev/null
+++ b/internal/adc/translator/annotations/plugins/fault_injection_test.go
@@ -0,0 +1,61 @@
+// 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 (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+func TestFaultInjectionHttpAllowMethods(t *testing.T) {
+ handler := NewFaultInjectionHandler()
+ assert.Equal(t, "fault-injection", handler.PluginName())
+
+ extractor := annotations.NewExtractor(map[string]string{
+ annotations.AnnotationsHttpAllowMethods: "GET,POST",
+ })
+
+ plugin, err := handler.Handle(extractor)
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+
+ data, err := json.Marshal(plugin)
+ assert.NoError(t, err)
+ assert.JSONEq(t,
`{"abort":{"http_status":405,"vars":[[["request_method","!","in",["GET","POST"]]]]}}`,
string(data))
+}
+
+func TestFaultInjectionHttpBlockMethods(t *testing.T) {
+ handler := NewFaultInjectionHandler()
+ assert.Equal(t, "fault-injection", handler.PluginName())
+
+ extractor := annotations.NewExtractor(map[string]string{
+ annotations.AnnotationsHttpBlockMethods: "GET,POST",
+ })
+
+ plugin, err := handler.Handle(extractor)
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+
+ data, err := json.Marshal(plugin)
+ assert.NoError(t, err)
+ assert.JSONEq(t,
`{"abort":{"http_status":405,"vars":[[["request_method","in",["GET","POST"]]]]}}`,
string(data))
+}
diff --git a/internal/adc/translator/annotations/plugins/plugins.go
b/internal/adc/translator/annotations/plugins/plugins.go
index fb0f0b27..2dd8e5f8 100644
--- a/internal/adc/translator/annotations/plugins/plugins.go
+++ b/internal/adc/translator/annotations/plugins/plugins.go
@@ -39,6 +39,7 @@ var (
NewRedirectHandler(),
NewCorsHandler(),
NewCSRFHandler(),
+ NewFaultInjectionHandler(),
}
)
diff --git a/internal/adc/translator/annotations_test.go
b/internal/adc/translator/annotations_test.go
index 4ff6bfe8..c94416d0 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -19,6 +19,7 @@ import (
"errors"
"testing"
+ "github.com/incubator4/go-resty-expr/expr"
"github.com/stretchr/testify/assert"
adctypes "github.com/apache/apisix-ingress-controller/api/adc"
@@ -216,6 +217,46 @@ func TestTranslateIngressAnnotations(t *testing.T) {
EnableWebsocket: true,
},
},
+ {
+ name: "fault injection by allowed http methods",
+ anno: map[string]string{
+ annotations.AnnotationsHttpAllowMethods:
"GET,POST",
+ },
+ expected: &IngressConfig{
+ Plugins: adctypes.Plugins{
+ "fault-injection":
&adctypes.FaultInjectionConfig{
+ Abort:
&adctypes.FaultInjectionAbortConfig{
+ HTTPStatus: 405,
+ Vars: [][]expr.Expr{{
+
expr.StringExpr("request_method").Not().In(
+
expr.ArrayExpr(expr.ExprArrayFromStrings([]string{"GET", "POST"})...),
+ ),
+ }},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "fault injection by blocked http methods",
+ anno: map[string]string{
+ annotations.AnnotationsHttpBlockMethods:
"DELETE",
+ },
+ expected: &IngressConfig{
+ Plugins: adctypes.Plugins{
+ "fault-injection":
&adctypes.FaultInjectionConfig{
+ Abort:
&adctypes.FaultInjectionAbortConfig{
+ HTTPStatus: 405,
+ Vars: [][]expr.Expr{{
+
expr.StringExpr("request_method").In(
+
expr.ArrayExpr(expr.ExprArrayFromStrings([]string{"DELETE"})...),
+ ),
+ }},
+ },
+ },
+ },
+ },
+ },
}
for _, tt := range tests {
diff --git a/internal/webhook/v1/ingress_webhook.go
b/internal/webhook/v1/ingress_webhook.go
index 6f7a6229..78bb4908 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -57,8 +57,6 @@ var unsupportedAnnotations = []string{
"k8s.apisix.apache.org/auth-client-headers",
"k8s.apisix.apache.org/allowlist-source-range",
"k8s.apisix.apache.org/blocklist-source-range",
- "k8s.apisix.apache.org/http-allow-methods",
- "k8s.apisix.apache.org/http-block-methods",
"k8s.apisix.apache.org/auth-type",
"k8s.apisix.apache.org/svc-namespace",
}
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 0a4857f5..ad559528 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -339,6 +339,49 @@ spec:
name: httpbin-service-e2e-test
port:
number: 80
+`
+ allowMethods = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: allow-methods
+ annotations:
+ k8s.apisix.apache.org/http-allow-methods: "GET,POST"
+spec:
+ ingressClassName: %s
+ rules:
+ - host: httpbin.example
+ http:
+ paths:
+ - path: /anything
+ pathType: Exact
+ backend:
+ service:
+ name: httpbin-service-e2e-test
+ port:
+ number: 80
+`
+
+ blockMethods = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: block-methods
+ annotations:
+ k8s.apisix.apache.org/http-block-methods: "DELETE"
+spec:
+ ingressClassName: %s
+ rules:
+ - host: httpbin2.example
+ http:
+ paths:
+ - path: /anything
+ pathType: Exact
+ backend:
+ service:
+ name: httpbin-service-e2e-test
+ port:
+ number: 80
`
)
BeforeEach(func() {
@@ -496,5 +539,76 @@ spec:
Expect(err).NotTo(HaveOccurred(), "unmarshalling echo
plugin config")
Expect(echoConfig["body"]).To(Equal("hello from plugin
config"), "checking echo plugin body")
})
+ It("methods", func() {
+
Expect(s.CreateResourceFromString(fmt.Sprintf(allowMethods,
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
Expect(s.CreateResourceFromString(fmt.Sprintf(blockMethods,
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+ tets := []*scaffold.RequestAssert{
+ {
+ Method: "GET",
+ Path: "/anything",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "POST",
+ Path: "/anything",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "PUT",
+ Path: "/anything",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
+ },
+ {
+ Method: "PATCH",
+ Path: "/anything",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
+ },
+ {
+ Method: "DELETE",
+ Path: "/anything",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
+ },
+ {
+ Method: "GET",
+ Path: "/anything",
+ Host: "httpbin2.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "POST",
+ Path: "/anything",
+ Host: "httpbin2.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "PUT",
+ Path: "/anything",
+ Host: "httpbin2.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "PATCH",
+ Path: "/anything",
+ Host: "httpbin2.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ },
+ {
+ Method: "DELETE",
+ Path: "/anything",
+ Host: "httpbin2.example",
+ Check:
scaffold.WithExpectedStatus(http.StatusMethodNotAllowed),
+ },
+ }
+
+ for _, test := range tets {
+ s.RequestAssert(test)
+ }
+ })
})
})
diff --git a/test/e2e/scaffold/assertion.go b/test/e2e/scaffold/assertion.go
index a7a18246..543780d7 100644
--- a/test/e2e/scaffold/assertion.go
+++ b/test/e2e/scaffold/assertion.go
@@ -18,6 +18,7 @@
package scaffold
import (
+ "encoding/json"
"fmt"
"io"
"net"
@@ -62,25 +63,26 @@ type HTTPResponse struct {
}
type BasicAuth struct {
- Username string
- Password string
+ Username string `json:"username"`
+ Password string `json:"password"`
}
type RequestAssert struct {
- Client *httpexpect.Expect
- Method string
- Path string
- Host string
- Query map[string]any
- Headers map[string]string
- Body []byte
- BasicAuth *BasicAuth
-
- Timeout time.Duration
- Interval time.Duration
-
- Check ResponseCheckFunc
- Checks []ResponseCheckFunc
+ Method string `json:"method,omitempty"`
+ Path string `json:"path,omitempty"`
+ Host string `json:"host,omitempty"`
+ Query map[string]any `json:"query,omitempty"`
+ Headers map[string]string `json:"headers,omitempty"`
+ Body []byte `json:"body,omitempty"`
+ BasicAuth *BasicAuth `json:"basic_auth,omitempty"`
+
+ Client *httpexpect.Expect `json:"-"`
+
+ Timeout time.Duration `json:"-"`
+ Interval time.Duration `json:"-"`
+
+ Check ResponseCheckFunc `json:"-"`
+ Checks []ResponseCheckFunc `json:"-"`
}
func (c *RequestAssert) request(method, path string, body []byte)
*httpexpect.Request {
@@ -308,7 +310,8 @@ func (s *Scaffold) RequestAssert(r *RequestAssert) bool {
for _, check := range r.Checks {
if err := check(resp); err != nil {
- return fmt.Errorf("response check failed: %w",
err)
+ req, _ := json.MarshalIndent(r, "", " ")
+ return fmt.Errorf("response check failed for
request %s: %v", string(req), err)
}
}
return nil