mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +08:00
feat: implement custom-response plugin in the golang version (#689)
This commit is contained in:
84
plugins/wasm-go/extensions/custom-response/README.md
Normal file
84
plugins/wasm-go/extensions/custom-response/README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<p>
|
||||||
|
<a href="README_EN.md"> English </a> | 中文
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# 功能说明
|
||||||
|
`custom-response`插件支持配置自定义的响应,包括自定义 HTTP 应答状态码、HTTP 应答头,以及 HTTP 应答 Body。可以用于 Mock 响应,也可以用于判断特定状态码后给出自定义应答,例如在触发网关限流策略时实现自定义响应。
|
||||||
|
|
||||||
|
# 配置字段
|
||||||
|
|
||||||
|
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||||
|
| -------- | -------- | -------- | -------- | -------- |
|
||||||
|
| status_code | number | 选填 | 200 | 自定义 HTTP 应答状态码 |
|
||||||
|
| headers | array of string | 选填 | - | 自定义 HTTP 应答头,key 和 value 用`=`分隔 |
|
||||||
|
| body | string | 选填 | - | 自定义 HTTP 应答 Body |
|
||||||
|
| enable_on_status | array of number | 选填 | - | 匹配原始状态码,生成自定义响应,不填写时,不判断原始状态码 |
|
||||||
|
|
||||||
|
# 配置示例
|
||||||
|
|
||||||
|
## Mock 应答场景
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status_code: 200
|
||||||
|
headers:
|
||||||
|
- Content-Type=application/json
|
||||||
|
- Hello=World
|
||||||
|
body: "{\"hello\":\"world\"}"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
根据该配置,请求将返回自定义应答如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Hello: World
|
||||||
|
Content-Length: 17
|
||||||
|
|
||||||
|
{"hello":"world"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 触发限流时自定义响应
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
enable_on_status:
|
||||||
|
- 429
|
||||||
|
status_code: 302
|
||||||
|
headers:
|
||||||
|
- Location=https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
触发网关限流时一般会返回 `429` 状态码,这时请求将返回自定义应答如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 302 Found
|
||||||
|
Location: https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
从而实现基于浏览器 302 重定向机制,将限流后的用户引导到其他页面,比如可以是一个 CDN 上的静态页面。
|
||||||
|
|
||||||
|
如果希望触发限流时,正常返回其他应答,参考 Mock 应答场景配置相应的字段即可。
|
||||||
|
|
||||||
|
## 对特定路由或域名开启
|
||||||
|
```yaml
|
||||||
|
# 使用 matchRules 字段进行细粒度规则配置
|
||||||
|
matchRules:
|
||||||
|
# 规则一:按 Ingress 名称匹配生效
|
||||||
|
- ingress:
|
||||||
|
- default/foo
|
||||||
|
- default/bar
|
||||||
|
body: "{\"hello\":\"world\"}"
|
||||||
|
# 规则二:按域名匹配生效
|
||||||
|
- domain:
|
||||||
|
- "*.example.com"
|
||||||
|
- test.com
|
||||||
|
enable_on_status:
|
||||||
|
- 429
|
||||||
|
status_code: 200
|
||||||
|
headers:
|
||||||
|
- Content-Type=application/json
|
||||||
|
body: "{\"errmsg\": \"rate limited\"}"
|
||||||
|
```
|
||||||
|
此例 `ingress` 中指定的 `default/foo` 和 `default/bar` 对应 default 命名空间下名为 foo 和 bar 的 Ingress,当匹配到这两个 Ingress 时,将使用此段配置;
|
||||||
|
此例 `domain` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
|
||||||
|
配置的匹配生效顺序,将按照 `matchRules` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。
|
||||||
84
plugins/wasm-go/extensions/custom-response/README_EN.md
Normal file
84
plugins/wasm-go/extensions/custom-response/README_EN.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<p>
|
||||||
|
English | <a href="README.md">中文</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Description
|
||||||
|
`custom-response` plugin implements a function of sending custom responses, including custom HTTP response status codes, HTTP response headers and HTTP response body, which can be used in the scenarios of response mocking and sending a custom response for specific status codes, such as customizing the response for rate-limited requests.
|
||||||
|
|
||||||
|
# Configuration Fields
|
||||||
|
|
||||||
|
| Name | Type | Requirement | Default Value | Description |
|
||||||
|
| -------- | -------- | -------- | -------- | -------- |
|
||||||
|
| status_code | number | Optional | 200 | Custom HTTP response status code |
|
||||||
|
| headers | array of string | Optional | - | Custom HTTP response header. Key and value shall be separated using `=`. |
|
||||||
|
| body | string | Optional | - | Custom HTTP response body |
|
||||||
|
| enable_on_status | array of number | Optional | - | The original response status code to match. Generate the custom response only the actual status code matches the configuration. Ignore the status code match if left unconfigured. |
|
||||||
|
|
||||||
|
# Configuration Samples
|
||||||
|
|
||||||
|
## Mock Responses
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
status_code: 200
|
||||||
|
headers:
|
||||||
|
- Content-Type=application/json
|
||||||
|
- Hello=World
|
||||||
|
body: "{\"hello\":\"world\"}"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
According to the configuration above, all the requests will get the following custom response:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
Hello: World
|
||||||
|
Content-Length: 17
|
||||||
|
|
||||||
|
{"hello":"world"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Send a Custom Response when Rate-Limited
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
enable_on_status:
|
||||||
|
- 429
|
||||||
|
status_code: 302
|
||||||
|
headers:
|
||||||
|
- Location=https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
When rate-limited, normally gateway will return a status code of `429` . Now, rate-limited requests will get the following custom response:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 302 Found
|
||||||
|
Location: https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
So based on the 302 redirecting mechanism provided by browsers, this can redirect rate-limited users to other pages, for example, a static page hosted on CDN.
|
||||||
|
|
||||||
|
If you'd like to send other responses when rate-limited, please add other fields into the configuration, referring to the Mock Responses scenario.
|
||||||
|
|
||||||
|
## Only Enabled for Specific Routes or Domains
|
||||||
|
```yaml
|
||||||
|
# Use matchRules field for fine-grained rule configurations
|
||||||
|
matchRules:
|
||||||
|
# Rule 1: Match by Ingress name
|
||||||
|
- ingress:
|
||||||
|
- default/foo
|
||||||
|
- default/bar
|
||||||
|
body: "{\"hello\":\"world\"}"
|
||||||
|
# Rule 2: Match by domain
|
||||||
|
- domain:
|
||||||
|
- "*.example.com"
|
||||||
|
- test.com
|
||||||
|
enable_on_status:
|
||||||
|
- 429
|
||||||
|
status_code: 200
|
||||||
|
headers:
|
||||||
|
- Content-Type=application/json
|
||||||
|
body: "{\"errmsg\": \"rate limited\"}"
|
||||||
|
```
|
||||||
|
In the rule sample of `ingress`, `default/foo` and `default/bar` are the Ingresses named foo and bar in the default namespace. When the current Ingress names matches the configuration, the rule following shall be applied.
|
||||||
|
In the rule sample of `domain`, `*.example.com` and `test.com` are the domain names used for request matching. When the current domain name matches the configuration, the rule following shall be applied.
|
||||||
|
All rules shall be checked following the order of items in the `matchRules` field, The first matched rule will be applied. All remained will be ignored.
|
||||||
71
plugins/wasm-go/extensions/custom-response/envoy.yaml
Normal file
71
plugins/wasm-go/extensions/custom-response/envoy.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
admin:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 9901
|
||||||
|
static_resources:
|
||||||
|
listeners:
|
||||||
|
- name: listener_0
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
protocol: TCP
|
||||||
|
address: 0.0.0.0
|
||||||
|
port_value: 10000
|
||||||
|
filter_chains:
|
||||||
|
- filters:
|
||||||
|
- name: envoy.filters.network.http_connection_manager
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||||
|
scheme_header_transformation:
|
||||||
|
scheme_to_overwrite: https
|
||||||
|
stat_prefix: ingress_http
|
||||||
|
route_config:
|
||||||
|
name: local_route
|
||||||
|
virtual_hosts:
|
||||||
|
- name: local_service
|
||||||
|
domains: ["*"]
|
||||||
|
routes:
|
||||||
|
- match:
|
||||||
|
prefix: "/"
|
||||||
|
route:
|
||||||
|
cluster: httpbin
|
||||||
|
http_filters:
|
||||||
|
- name: wasmdemo
|
||||||
|
typed_config:
|
||||||
|
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||||
|
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||||
|
value:
|
||||||
|
config:
|
||||||
|
name: wasmdemo
|
||||||
|
vm_config:
|
||||||
|
runtime: envoy.wasm.runtime.v8
|
||||||
|
code:
|
||||||
|
local:
|
||||||
|
filename: /etc/envoy/main.wasm
|
||||||
|
configuration:
|
||||||
|
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||||
|
value: |
|
||||||
|
{
|
||||||
|
"headers": ["key1=value1", "key2=value2"],
|
||||||
|
"status_code": 200,
|
||||||
|
"enable_on_status": [200, 201],
|
||||||
|
"body": "{\"hello\":\"world\"}"
|
||||||
|
}
|
||||||
|
- name: envoy.filters.http.router
|
||||||
|
clusters:
|
||||||
|
- name: httpbin
|
||||||
|
connect_timeout: 30s
|
||||||
|
type: LOGICAL_DNS
|
||||||
|
# Comment out the following line to test on v6 networks
|
||||||
|
dns_lookup_family: V4_ONLY
|
||||||
|
lb_policy: ROUND_ROBIN
|
||||||
|
load_assignment:
|
||||||
|
cluster_name: httpbin
|
||||||
|
endpoints:
|
||||||
|
- lb_endpoints:
|
||||||
|
- endpoint:
|
||||||
|
address:
|
||||||
|
socket_address:
|
||||||
|
address: httpbin
|
||||||
|
port_value: 80
|
||||||
20
plugins/wasm-go/extensions/custom-response/go.mod
Normal file
20
plugins/wasm-go/extensions/custom-response/go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module github.com/alibaba/higress/plugins/wasm-go/extensions/basic-auth
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||||
|
github.com/tidwall/gjson v1.14.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||||
|
github.com/magefile/mage v1.14.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
)
|
||||||
15
plugins/wasm-go/extensions/custom-response/go.sum
Normal file
15
plugins/wasm-go/extensions/custom-response/go.sum
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||||
|
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||||
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
|
||||||
|
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
|
||||||
|
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||||
|
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
135
plugins/wasm-go/extensions/custom-response/main.go
Normal file
135
plugins/wasm-go/extensions/custom-response/main.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||||
|
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||||
|
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"custom-response",
|
||||||
|
wrapper.ParseConfigBy(parseConfig),
|
||||||
|
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||||
|
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomResponseConfig struct {
|
||||||
|
statusCode uint32
|
||||||
|
headers [][2]string
|
||||||
|
body string
|
||||||
|
enableOnStatus []uint32
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(gjson gjson.Result, config *CustomResponseConfig, log wrapper.Log) error {
|
||||||
|
headersArray := gjson.Get("headers").Array()
|
||||||
|
config.headers = make([][2]string, 0, len(headersArray))
|
||||||
|
for _, v := range headersArray {
|
||||||
|
kv := strings.SplitN(v.String(), "=", 2)
|
||||||
|
if len(kv) == 2 {
|
||||||
|
key := strings.TrimSpace(kv[0])
|
||||||
|
value := strings.TrimSpace(kv[1])
|
||||||
|
if strings.EqualFold(key, "content-type") {
|
||||||
|
config.contentType = value
|
||||||
|
} else if strings.EqualFold(key, "content-length") {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
config.headers = append(config.headers, [2]string{key, value})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid header pair format: %s", v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.body = gjson.Get("body").String()
|
||||||
|
if config.contentType == "" && config.body != "" {
|
||||||
|
if json.Valid([]byte(config.body)) {
|
||||||
|
config.contentType = "application/json; charset=utf-8"
|
||||||
|
} else {
|
||||||
|
config.contentType = "text/plain; charset=utf-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.headers = append(config.headers, [2]string{"content-type", config.contentType})
|
||||||
|
|
||||||
|
config.statusCode = 200
|
||||||
|
if gjson.Get("status_code").Exists() {
|
||||||
|
statusCode := gjson.Get("status_code")
|
||||||
|
parsedStatusCode, err := strconv.Atoi(statusCode.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid status code value: %s", statusCode.String())
|
||||||
|
}
|
||||||
|
config.statusCode = uint32(parsedStatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableOnStatusArray := gjson.Get("enable_on_status").Array()
|
||||||
|
config.enableOnStatus = make([]uint32, 0, len(enableOnStatusArray))
|
||||||
|
for _, v := range enableOnStatusArray {
|
||||||
|
parsedEnableOnStatus, err := strconv.Atoi(v.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid enable_on_status value: %s", v.String())
|
||||||
|
}
|
||||||
|
config.enableOnStatus = append(config.enableOnStatus, uint32(parsedEnableOnStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||||
|
if len(config.enableOnStatus) != 0 {
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
err := proxywasm.SendHttpResponse(config.statusCode, config.headers, []byte(config.body), -1)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("send http response failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionPause
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpResponseHeaders(ctx wrapper.HttpContext, config CustomResponseConfig, log wrapper.Log) types.Action {
|
||||||
|
// enableOnStatus is not empty, compare the status code.
|
||||||
|
// if match the status code, mock the response.
|
||||||
|
statusCodeStr, err := proxywasm.GetHttpResponseHeader(":status")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get http response status code failed: %v", err)
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
statusCode, err := strconv.ParseUint(statusCodeStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("parse http response status code failed: %v", err)
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range config.enableOnStatus {
|
||||||
|
if uint32(statusCode) == v {
|
||||||
|
err = proxywasm.SendHttpResponse(config.statusCode, config.headers, []byte(config.body), -1)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("send http response failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
159
test/e2e/conformance/tests/go-wasm-custom-response.go
Normal file
159
test/e2e/conformance/tests/go-wasm-custom-response.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed 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 tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||||
|
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(WasmPluginsCustomResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
var WasmPluginsCustomResponse = suite.ConformanceTest{
|
||||||
|
ShortName: "WasmPluginsCustomResponse",
|
||||||
|
Description: "The Ingress in the higress-conformance-infra namespace test the custom-response WASM plugin.",
|
||||||
|
Manifests: []string{"tests/go-wasm-custom-response.yaml"},
|
||||||
|
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
|
||||||
|
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
|
||||||
|
testcases := []http.Assertion{
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TestCaseName: "case 1: Match global config",
|
||||||
|
CompareTarget: http.CompareTargetResponse,
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "foo.com",
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
},
|
||||||
|
ContentType: http.ContentTypeApplicationJson,
|
||||||
|
Body: []byte("{\"hello\":\"foo\"}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TestCaseName: "case 2: Match rule config",
|
||||||
|
CompareTarget: http.CompareTargetResponse,
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "bar.com",
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
ContentType: http.ContentTypeApplicationJson,
|
||||||
|
Body: []byte("{\"hello\":\"bar\"}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TestCaseName: "case 3: Match enable_on_status",
|
||||||
|
CompareTarget: http.CompareTargetResponse,
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "baz.com",
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"key3": "value3",
|
||||||
|
},
|
||||||
|
ContentType: http.ContentTypeApplicationJson,
|
||||||
|
Body: []byte("{\"hello\":\"baz\"}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TestCaseName: "case 4: Not match enable_on_status",
|
||||||
|
TargetBackend: "infra-backend-v1",
|
||||||
|
TargetNamespace: "higress-conformance-infra",
|
||||||
|
CompareTarget: http.CompareTargetRequest,
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "baz.com",
|
||||||
|
Path: "/",
|
||||||
|
Headers: map[string]string{"Authorization": "Basic YWRtaW46MTIzNDU2"}, // base64("admin:123456")
|
||||||
|
},
|
||||||
|
ExpectedRequest: &http.ExpectedRequest{
|
||||||
|
Request: http.Request{
|
||||||
|
Host: "baz.com",
|
||||||
|
Path: "/",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"X-Mse-Consumer": "consumer1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Meta: http.AssertionMeta{
|
||||||
|
TestCaseName: "case 5: Change status code",
|
||||||
|
CompareTarget: http.CompareTargetResponse,
|
||||||
|
},
|
||||||
|
Request: http.AssertionRequest{
|
||||||
|
ActualRequest: http.Request{
|
||||||
|
Host: "qux.com",
|
||||||
|
Path: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Response: http.AssertionResponse{
|
||||||
|
ExpectedResponse: http.Response{
|
||||||
|
StatusCode: 201,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"key5": "value5",
|
||||||
|
},
|
||||||
|
ContentType: http.ContentTypeApplicationJson,
|
||||||
|
Body: []byte("{\"hello\":\"qux\"}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Run("WasmPlugins custom-response", func(t *testing.T) {
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
118
test/e2e/conformance/tests/go-wasm-custom-response.yaml
Normal file
118
test/e2e/conformance/tests/go-wasm-custom-response.yaml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
#
|
||||||
|
# Licensed 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.
|
||||||
|
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: wasmplugin-custom-response
|
||||||
|
namespace: higress-conformance-infra
|
||||||
|
spec:
|
||||||
|
ingressClassName: higress
|
||||||
|
rules:
|
||||||
|
- host: "foo.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: infra-backend-v1
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
- host: "bar.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: infra-backend-v1
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
- host: "baz.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: infra-backend-v1
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
- host: "qux.com"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- pathType: Prefix
|
||||||
|
path: "/"
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: infra-backend-v1
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
---
|
||||||
|
apiVersion: extensions.higress.io/v1alpha1
|
||||||
|
kind: WasmPlugin
|
||||||
|
metadata:
|
||||||
|
name: custom-response
|
||||||
|
namespace: higress-system
|
||||||
|
spec:
|
||||||
|
priority: 200 # ensure custom-response plugin is applied before basic-auth plugin
|
||||||
|
defaultConfig:
|
||||||
|
headers:
|
||||||
|
- key1=value1
|
||||||
|
"body": "{\"hello\":\"foo\"}"
|
||||||
|
matchRules:
|
||||||
|
- domain:
|
||||||
|
- bar.com
|
||||||
|
config:
|
||||||
|
headers:
|
||||||
|
- key2=value2
|
||||||
|
"body": "{\"hello\":\"bar\"}"
|
||||||
|
- domain:
|
||||||
|
- baz.com
|
||||||
|
config:
|
||||||
|
enable_on_status:
|
||||||
|
- 401
|
||||||
|
headers:
|
||||||
|
- key3=value3
|
||||||
|
"body": "{\"hello\":\"baz\"}"
|
||||||
|
- domain:
|
||||||
|
- qux.com
|
||||||
|
config:
|
||||||
|
status_code: 201
|
||||||
|
headers:
|
||||||
|
- key5=value5
|
||||||
|
"body": "{\"hello\":\"qux\"}"
|
||||||
|
url: file:///opt/plugins/wasm-go/extensions/custom-response/plugin.wasm
|
||||||
|
---
|
||||||
|
# test enable_on_status 401
|
||||||
|
apiVersion: extensions.higress.io/v1alpha1
|
||||||
|
kind: WasmPlugin
|
||||||
|
metadata:
|
||||||
|
name: basic-auth
|
||||||
|
namespace: higress-system
|
||||||
|
spec:
|
||||||
|
priority: 100
|
||||||
|
defaultConfig:
|
||||||
|
consumers:
|
||||||
|
- credential: admin:123456
|
||||||
|
name: consumer1
|
||||||
|
global_auth: false
|
||||||
|
matchRules:
|
||||||
|
- config:
|
||||||
|
allow:
|
||||||
|
- consumer1
|
||||||
|
domain:
|
||||||
|
- baz.com
|
||||||
|
url: file:///opt/plugins/wasm-go/extensions/basic-auth/plugin.wasm
|
||||||
Reference in New Issue
Block a user