Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/adc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
64 changes: 64 additions & 0 deletions internal/adc/translator/annotations/plugins/fault-injection.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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))
}
1 change: 1 addition & 0 deletions internal/adc/translator/annotations/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var (
handlers = []PluginAnnotationsHandler{
NewRedirectHandler(),
NewCorsHandler(),
NewFaultInjectionHandler(),
}
)

Expand Down
41 changes: 41 additions & 0 deletions internal/adc/translator/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions internal/webhook/v1/ingress_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,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",
}
Expand Down
114 changes: 114 additions & 0 deletions test/e2e/ingress/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,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() {
Expand Down Expand Up @@ -359,5 +402,76 @@ spec:
Status(http.StatusPermanentRedirect).
Header("Location").IsEqual("/anything/ip")
})
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)
}
})
})
})
Loading
Loading