mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +08:00
Add wasm plugin contribution introduction (#47)
This commit is contained in:
2
plugins/wasm-go/Dockerfile
Normal file
2
plugins/wasm-go/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM scratch
|
||||
COPY main.wasm plugin.wasm
|
||||
@@ -1,48 +1,109 @@
|
||||
## Intro
|
||||
[English](./README_EN.md)
|
||||
|
||||
This SDK is used to develop the WASM Plugins of Higress.
|
||||
## Requirements
|
||||
## 介绍
|
||||
|
||||
(need support Go's type parameters)
|
||||
此 SDK 用于开发 Higress 的 Wasm 插件
|
||||
|
||||
Go version: >= 1.18
|
||||
## 编译环境要求
|
||||
|
||||
TinyGo version: >= 0.25.0
|
||||
(需要支持 go 范型特性)
|
||||
|
||||
Go 版本: >= 1.18
|
||||
|
||||
TinyGo 版本: >= 0.25.0
|
||||
|
||||
## Quick Examples
|
||||
|
||||
### wasm plugin config
|
||||
使用 [request-block](extensions/request-block) 作为例子
|
||||
|
||||
```yaml
|
||||
# this config will take effect globally (all incoming requests are affected)
|
||||
block_urls:
|
||||
- "test"
|
||||
_rules_:
|
||||
# matching by route name takes effect
|
||||
- _match_route_:
|
||||
- route-a
|
||||
- route-b
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
# matching by domain takes effect
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
```
|
||||
|
||||
### code
|
||||
|
||||
[request-block](example/request-block)
|
||||
|
||||
|
||||
### compile to wasm
|
||||
### step1. 编译 wasm
|
||||
|
||||
```bash
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi ./main.go
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi ./extensions/request-block/main.go
|
||||
```
|
||||
|
||||
### step2. 构建并推送插件的 docker 镜像
|
||||
|
||||
使用这份简单的 [dockerfile](./Dockerfile).
|
||||
|
||||
```bash
|
||||
docker build -t <your_registry_hub>/request-block:1.0.0 .
|
||||
docker push <your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
### step3. 创建 WasmPlugin 资源
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.istio.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: request-block
|
||||
namespace: higress-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
higress: higress-system-higress-gateway
|
||||
pluginConfig:
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
创建上述资源后,如果请求url携带 `swagger.html`, 则这个请求就会被拒绝,例如:
|
||||
|
||||
```bash
|
||||
curl <your_gateway_address>/api/user/swagger.html
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP/1.1 403 Forbidden
|
||||
date: Wed, 09 Nov 2022 12:12:32 GMT
|
||||
server: istio-envoy
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
如果需要进一步控制插件的执行阶段和顺序
|
||||
|
||||
可以阅读此 [文档](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) 了解更多关于 wasmplugin 的配置
|
||||
|
||||
|
||||
## 路由级或域名级生效
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.istio.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: request-block
|
||||
namespace: higress-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
higress: higress-system-higress-gateway
|
||||
pluginConfig:
|
||||
# 跟上面例子一样,这个配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
_rules_:
|
||||
# 路由级生效配置
|
||||
- _match_route_:
|
||||
- default/foo
|
||||
# default 命名空间下名为 foo 的 ingress 会执行下面这个配置
|
||||
block_bodys:
|
||||
- "foo"
|
||||
- _match_route_:
|
||||
- default/bar
|
||||
# default 命名空间下名为 bar 的 ingress 会执行下面这个配置
|
||||
block_bodys:
|
||||
- "bar"
|
||||
# 域名级生效配置
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
# 若请求匹配了上面的域名, 会执行下面这个配置
|
||||
block_bodys:
|
||||
- "foo"
|
||||
- "bar"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
所有规则会按上面配置的顺序一次执行匹配,当有一个规则匹配时,就停止匹配,并选择匹配的配置执行插件逻辑
|
||||
|
||||
|
||||
103
plugins/wasm-go/README_EN.md
Normal file
103
plugins/wasm-go/README_EN.md
Normal file
@@ -0,0 +1,103 @@
|
||||
## Intro
|
||||
|
||||
This SDK is used to develop the WASM Plugins of Higress.
|
||||
## Requirements
|
||||
|
||||
(need support Go's type parameters)
|
||||
|
||||
Go version: >= 1.18
|
||||
|
||||
TinyGo version: >= 0.25.0
|
||||
|
||||
## Quick Examples
|
||||
|
||||
Use the [request-block](example/request-block) as an example
|
||||
|
||||
### step1. compile to wasm
|
||||
|
||||
```bash
|
||||
tinygo build -o main.wasm -scheduler=none -target=wasi ./example/request-block/main.go
|
||||
```
|
||||
|
||||
### step2. build&push docker image
|
||||
|
||||
Use this [dockerfile](./Dockerfile).
|
||||
|
||||
```bash
|
||||
docker build -t <your_registry_hub>/request-block:1.0.0 .
|
||||
docker push <your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
### step3. create WasmPlugin resource
|
||||
|
||||
Read this [document](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) to learn more about wasmplugin.
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.istio.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: request-block
|
||||
namespace: higress-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
higress: higress-system-higress-gateway
|
||||
pluginConfig:
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
If the url in request contains the `swagger.html`, the request will be blocked.
|
||||
|
||||
```bash
|
||||
curl <your_gateway_address>/api/user/swagger.html
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP/1.1 403 Forbidden
|
||||
date: Wed, 09 Nov 2022 12:12:32 GMT
|
||||
server: istio-envoy
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
## route-level & domain-level takes effect
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions.istio.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
name: request-block
|
||||
namespace: higress-system
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
higress: higress-system-higress-gateway
|
||||
pluginConfig:
|
||||
# this config will take effect globally (all incoming requests not matched by rules below)
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
_rules_:
|
||||
# route-level takes effect
|
||||
- _match_route_:
|
||||
- default/foo
|
||||
# the ingress foo in namespace default will use this config
|
||||
block_bodys:
|
||||
- "foo"
|
||||
- _match_route_:
|
||||
- default/bar
|
||||
# the ingress bar in namespace default will use this config
|
||||
block_bodys:
|
||||
- "bar"
|
||||
# domain-level takes effect
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
# if the request's domain matched, this config will be used
|
||||
block_bodys:
|
||||
- "foo"
|
||||
- "bar"
|
||||
url: oci://<your_registry_hub>/request-block:1.0.0
|
||||
```
|
||||
|
||||
The rules will be matched in the order of configuration. If one match is found, it will stop, and the matching configuration will take effect.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/hello-world
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/hello-world
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -31,7 +31,7 @@ func main() {
|
||||
type HelloWorldConfig struct {
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config HelloWorldConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[HelloWorldConfig], config HelloWorldConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
err := proxywasm.AddHttpRequestHeader("hello", "world")
|
||||
if err != nil {
|
||||
log.Critical("failed to set request header")
|
||||
@@ -1,11 +1,11 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/request-block
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/http-call
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -96,7 +96,7 @@ func parseConfig(json gjson.Result, config *HttpCallConfig, log wrapper.LogWrapp
|
||||
}
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config HttpCallConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[HttpCallConfig], config HttpCallConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
config.client.Get(config.requestPath, nil,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
defer proxywasm.ResumeHttpRequest()
|
||||
@@ -1,11 +1,11 @@
|
||||
module github.com/mse-group/wasm-extensions-go/example/http-call
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/request-block
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/mse-group/wasm-extensions-go => ../..
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/mse-group/wasm-extensions-go v0.0.0
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/wrapper"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -93,7 +93,7 @@ func parseConfig(json gjson.Result, config *RequestBlockConfig, log wrapper.LogW
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(contextID uint32, config RequestBlockConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
func onHttpRequestHeaders(ctx *wrapper.CommonHttpCtx[RequestBlockConfig], config RequestBlockConfig, needBody *bool, log wrapper.LogWrapper) types.Action {
|
||||
if len(config.blockUrls) > 0 {
|
||||
requestUrl, err := proxywasm.GetHttpRequestHeader(":path")
|
||||
if err != nil {
|
||||
@@ -137,7 +137,7 @@ func onHttpRequestHeaders(contextID uint32, config RequestBlockConfig, needBody
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func onHttpRequestBody(contextID uint32, config RequestBlockConfig, body []byte, log wrapper.LogWrapper) types.Action {
|
||||
func onHttpRequestBody(ctx *wrapper.CommonHttpCtx[RequestBlockConfig], config RequestBlockConfig, body []byte, log wrapper.LogWrapper) types.Action {
|
||||
bodyStr := string(body)
|
||||
if !config.caseSensitive {
|
||||
bodyStr = strings.ToLower(bodyStr)
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/mse-group/wasm-extensions-go
|
||||
module github.com/alibaba/higress/plugins/wasm-go
|
||||
|
||||
go 1.18
|
||||
|
||||
|
||||
@@ -21,17 +21,18 @@ import (
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/mse-group/wasm-extensions-go/pkg/matcher"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/matcher"
|
||||
)
|
||||
|
||||
type ParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log LogWrapper) error
|
||||
type onHttpHeadersFunc[PluginConfig any] func(contextID uint32, config PluginConfig, needBody *bool, log LogWrapper) types.Action
|
||||
type onHttpBodyFunc[PluginConfig any] func(contextID uint32, config PluginConfig, body []byte, log LogWrapper) types.Action
|
||||
type onHttpHeadersFunc[PluginConfig any] func(context *CommonHttpCtx[PluginConfig], config PluginConfig, needBody *bool, log LogWrapper) types.Action
|
||||
type onHttpBodyFunc[PluginConfig any] func(context *CommonHttpCtx[PluginConfig], config PluginConfig, body []byte, log LogWrapper) types.Action
|
||||
|
||||
type CommonVmCtx[PluginConfig any] struct {
|
||||
types.DefaultVMContext
|
||||
pluginName string
|
||||
log LogWrapper
|
||||
hasCustomConfig bool
|
||||
parseConfig ParseConfigFunc[PluginConfig]
|
||||
onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
|
||||
onHttpRequestBody onHttpBodyFunc[PluginConfig]
|
||||
@@ -81,8 +82,9 @@ func parseEmptyPluginConfig[PluginConfig any](gjson.Result, *PluginConfig, LogWr
|
||||
|
||||
func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] {
|
||||
ctx := &CommonVmCtx[PluginConfig]{
|
||||
pluginName: pluginName,
|
||||
log: LogWrapper{pluginName},
|
||||
pluginName: pluginName,
|
||||
log: LogWrapper{pluginName},
|
||||
hasCustomConfig: true,
|
||||
}
|
||||
for _, set := range setFuncs {
|
||||
set(ctx)
|
||||
@@ -94,6 +96,7 @@ func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFu
|
||||
ctx.log.Critical(msg)
|
||||
panic(msg)
|
||||
}
|
||||
ctx.hasCustomConfig = false
|
||||
ctx.parseConfig = parseEmptyPluginConfig[PluginConfig]
|
||||
}
|
||||
return ctx
|
||||
@@ -117,16 +120,20 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart
|
||||
ctx.vm.log.Criticalf("error reading plugin configuration: %v", err)
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
var jsonData gjson.Result
|
||||
if len(data) == 0 {
|
||||
ctx.vm.log.Warn("need config")
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
if !gjson.ValidBytes(data) {
|
||||
ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data))
|
||||
return types.OnPluginStartStatusFailed
|
||||
if ctx.vm.hasCustomConfig {
|
||||
ctx.vm.log.Warn("need config")
|
||||
return types.OnPluginStartStatusFailed
|
||||
}
|
||||
} else {
|
||||
if !gjson.ValidBytes(data) {
|
||||
ctx.vm.log.Warnf("the plugin configuration is not a valid json: %s", string(data))
|
||||
return types.OnPluginStartStatusFailed
|
||||
|
||||
}
|
||||
jsonData = gjson.ParseBytes(data)
|
||||
}
|
||||
jsonData := gjson.ParseBytes(data)
|
||||
err = ctx.ParseRuleConfig(jsonData, func(js gjson.Result, cfg *PluginConfig) error {
|
||||
return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
|
||||
})
|
||||
@@ -139,8 +146,9 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart
|
||||
|
||||
func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {
|
||||
httpCtx := &CommonHttpCtx[PluginConfig]{
|
||||
plugin: ctx,
|
||||
contextID: contextID,
|
||||
plugin: ctx,
|
||||
contextID: contextID,
|
||||
userContext: map[string]interface{}{},
|
||||
}
|
||||
if ctx.vm.onHttpRequestBody != nil {
|
||||
httpCtx.needRequestBody = true
|
||||
@@ -160,6 +168,15 @@ type CommonHttpCtx[PluginConfig any] struct {
|
||||
requestBodySize int
|
||||
responseBodySize int
|
||||
contextID uint32
|
||||
userContext map[string]interface{}
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) SetContext(key string, value interface{}) {
|
||||
ctx.userContext[key] = value
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) GetContext(key string) interface{} {
|
||||
return ctx.userContext[key]
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
@@ -175,7 +192,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, end
|
||||
if ctx.plugin.vm.onHttpRequestHeaders == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpRequestHeaders(ctx.contextID, *config,
|
||||
return ctx.plugin.vm.onHttpRequestHeaders(ctx, *config,
|
||||
&ctx.needRequestBody, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
@@ -198,7 +215,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStr
|
||||
ctx.plugin.vm.log.Warnf("get request body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpRequestBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
|
||||
return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
|
||||
@@ -208,7 +225,7 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, en
|
||||
if ctx.plugin.vm.onHttpResponseHeaders == nil {
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpResponseHeaders(ctx.contextID, *ctx.config,
|
||||
return ctx.plugin.vm.onHttpResponseHeaders(ctx, *ctx.config,
|
||||
&ctx.needResponseBody, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
@@ -231,5 +248,5 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfSt
|
||||
ctx.plugin.vm.log.Warnf("get response body failed: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return ctx.plugin.vm.onHttpResponseBody(ctx.contextID, *ctx.config, body, ctx.plugin.vm.log)
|
||||
return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user