(要求 1.18 版本以上)
官方指引链接:https://go.dev/doc/install
Program Files 或 Program Files (x86) 目录go version,输出当前安装的版本,表明安装成功
/usr/local/go目录go version,输出当前安装的版本,表明安装成功
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version,输出当前安装的版本,表明安装成功(要求 0.25.0 版本以上)
官方指引链接:https://tinygo.org/getting-started/install/
C:\tinygo,则需要将C:\tinygo\bin添加到环境变量PATH中,例如通过在命令窗口中输入 set 命令设置set PATH=%PATH%;"C:\tinygo\bin";
wget https://github.com/tinygo-org/tinygo/releases/download/v0.25.0/tinygo0.25.0.darwin-amd64.tar.gz
tar -zxf tinygo0.25.0.darwin-amd64.tar.gz
/tmp,则需要将/tmp/tinygo/bin添加到环境变量PATH中:export PATH=/tmp/tinygo/bin:$PATH
以 Ubuntu 下 amd64 架构为例,其他系统请参考官方指引链接
wget https://github.com/tinygo-org/tinygo/releases/download/v0.25.0/tinygo_0.25.0_amd64.deb
sudo dpkg -i tinygo_0.25.0_amd64.deb
export PATH=$PATH:/usr/local/bin
tinygo version,输出当前安装的版本,表明安装成功你可以在 higress 项目的 plugins/wasm-go 目录下创建新的插件工程目录,以便使用该目录下提供的脚手架工具(见 1.1); 或者新建一个 Go 项目,从头创建一个工程目录(见 1.2)。 如果你是第一次开发 wasm-go 插件,建议采取前者。
git clone https://github.com/alibaba/higress.git 将 higress 项目克隆到本地;cd plugins/wasm-go; mkdir wasm-demo-go, 进入项目的 plugins/wasm-go 目录,创建 wasm-demo-go 目录。wasm-demo-gogo mod init wasm-demo-go
go env -w GOPROXY=https://proxy.golang.com.cn,direct
go get github.com/tetratelabs/proxy-wasm-go-sdk
go get github.com/alibaba/higress/plugins/wasm-go@main
go get github.com/tidwall/gjson
下面是一个简单示例,实现了在插件配置mockEnable: true时直接返回hello world应答;未做插件配置,或者设置mockEnable: false时给原始请求添加 hello: world请求头。更多例子请参考本文第四节。
注意:在网关控制台中的插件配置为 yaml 格式,下发给插件时将自动转换为 json 格式,所以例子中的 parseConfig 可以直接从 json 中解析配置
package main
import (
        "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(
                // 插件名称
                "my-plugin",
                // 为解析插件配置,设置自定义函数
                wrapper.ParseConfigBy(parseConfig),
                // 为处理请求头,设置自定义函数
                wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
        )
}
// 自定义插件配置
type MyConfig struct {
        mockEnable bool
}
// 在控制台插件配置中填写的yaml配置会自动转换为json,此处直接从json这个参数里解析配置即可
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
        // 解析出配置,更新到config中
    	config.mockEnable = json.Get("mockEnable").Bool()
        return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
        proxywasm.AddHttpRequestHeader("hello", "world")
        if config.mockEnable {
                proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
        }
        return types.ActionContinue
}
上面示例代码中通过 wrapper.ProcessRequestHeadersBy将自定义函数 onHttpRequestHeaders用于HTTP 请求头处理阶段处理请求。除此之外,还可以通过下面方式,设置其他阶段的自定义处理函数
| HTTP 处理阶段 | 触发时机 | 挂载方法 | 
|---|---|---|
| HTTP 请求头处理阶段 | 网关接收到客户端发送来的请求头数据时 | wrapper.ProcessRequestHeadersBy | 
| HTTP 请求 Body 处理阶段 | 网关接收到客户端发送来的请求 Body 数据时 | wrapper.ProcessRequestBodyBy | 
| HTTP 应答头处理阶段 | 网关接收到后端服务响应的应答头数据时 | wrapper.ProcessResponseHeadersBy | 
| HTTP 应答 Body 处理阶段 | 网关接收到后端服务响应的应答 Body 数据时 | wrapper.ProcessResponseBodyBy | 
上面示例代码中的 proxywasm.AddHttpRequestHeader 和 proxywasm.SendHttpResponse是插件 SDK 提供的两个工具方法,主要的工具方法见下表:
| 分类 | 方法名称 | 用途 | 可以生效的 HTTP 处理阶段  | 
|---|---|---|---|
| 请求头处理 | GetHttpRequestHeaders | 获取客户端请求的全部请求头 | HTTP 请求头处理阶段 | 
| ReplaceHttpRequestHeaders | 替换客户端请求的全部请求头 | HTTP 请求头处理阶段 | |
| GetHttpRequestHeader | 获取客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| RemoveHttpRequestHeader | 移除客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| ReplaceHttpRequestHeader | 替换客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| AddHttpRequestHeader | 新增一个客户端请求头 | HTTP 请求头处理阶段 | |
| 请求 Body 处理 | GetHttpRequestBody | 获取客户端请求 Body | HTTP 请求 Body 处理阶段 | 
| AppendHttpRequestBody | 将指定的字节串附加到客户端请求 Body 末尾 | HTTP 请求 Body 处理阶段 | |
| PrependHttpRequestBody | 将指定的字节串附加到客户端请求 Body 的开头 | HTTP 请求 Body 处理阶段 | |
| ReplaceHttpRequestBody | 替换客户端请求 Body | HTTP 请求 Body 处理阶段 | |
| 应答头处理 | GetHttpResponseHeaders | 获取后端响应的全部应答头 | HTTP 应答头处理阶段 | 
| ReplaceHttpResponseHeaders | 替换后端响应的全部应答头 | HTTP 应答头处理阶段 | |
| GetHttpResponseHeader | 获取后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| RemoveHttpResponseHeader | 移除后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| ReplaceHttpResponseHeader | 替换后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| AddHttpResponseHeader | 新增一个后端响应头 | HTTP 应答头处理阶段 | |
| 应答 Body 处理 | GetHttpResponseBody | 获取客户端请求 Body | HTTP 应答 Body 处理阶段 | 
| AppendHttpResponseBody | 将指定的字节串附加到后端响应 Body 末尾 | HTTP 应答 Body 处理阶段 | |
| PrependHttpResponseBody | 将指定的字节串附加到后端响应 Body 的开头 | HTTP 应答 Body 处理阶段 | |
| ReplaceHttpResponseBody | 替换后端响应 Body | HTTP 应答 Body 处理阶段 | |
| HTTP 调用 | DispatchHttpCall | 发送一个 HTTP 请求 | - | 
| GetHttpCallResponseHeaders | 获取 DispatchHttpCall 请求响应的应答头 | - | |
| GetHttpCallResponseBody | 获取 DispatchHttpCall 请求响应的应答 Body | - | |
| GetHttpCallResponseTrailers | 获取 DispatchHttpCall 请求响应的应答 Trailer | - | |
| 直接响应 | SendHttpResponse | 直接返回一个特定的 HTTP 应答 | - | 
| 流程恢复 | ResumeHttpRequest | 恢复先前被暂停的请求处理流程 | - | 
| ResumeHttpResponse | 恢复先前被暂停的应答处理流程 | - | 
如果你的工程目录在 plugins/wasm-go 目录下, 见 3.1; 如果你是使用自行初始化的目录,见 3.2.
使用以下命令可以快速构建 wasm-go 插件:
$ PLUGIN_NAME=wasm-demo-go make build
... ...
image:            wasm-demo-go:20230223-173305-3b1a471
output wasm file: extensions/wasm-demo-go/plugin.wasm
该命令最终构建出一个 wasm 文件和一个 Docker image。
这个本地的 wasm 文件被输出到了指定的插件的目录下,可以直接用于本地调试。
你也可以直接使用 make build-push 一并构建和推送 image.
更多构建相关信息参见 plugins/wasm-go.
执行以下命令
tinygo build -o main.wasm -scheduler=none -target=wasi ./main.go
编译成功会在当前目录下创建文件 main.wasm。这个文件在下面本地调试的例子中也会被用到。
在使用云原生网关插件市场的自定义插件功能时,直接上传该文件即可。
TBD
插件无需配置时,直接定义空结构体即可
package main
import (
        "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"
)
func main() {
        wrapper.SetCtx(
                "hello-world",
                wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
        )
}
type MyConfig struct {}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
        proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
        return types.ActionContinue
}
目前仅支持 http 调用,支持访问在网关控制台中设置了服务来源的 Nacos、K8s 服务,以及固定地址或 DNS 来源的服务。请注意,无法直接使用net/http库中的 HTTP client,必须使用如下例中封装的 HTTP client。
下面例子中,在配置解析阶段解析服务类型,生成对应的 HTTP client ;在请求头处理阶段根据配置的请求路径访问对应服务,解析应答头,然后再设置在原始的请求头中。
package main
import (
	"errors"
	"net/http"
	"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(
		"http-call",
		wrapper.ParseConfigBy(parseConfig),
		wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
	)
}
type MyConfig struct {
	// 用于发起HTTP调用client
	client      wrapper.HttpClient
	// 请求url
	requestPath string
	// 根据这个key取出调用服务的应答头对应字段,再设置到原始请求的请求头,key为此配置项
	tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
	config.tokenHeader = json.Get("tokenHeader").String()
	if config.tokenHeader == "" {
		return errors.New("missing tokenHeader in config")
	}
	config.requestPath = json.Get("requestPath").String()
	if config.requestPath == "" {
		return errors.New("missing requestPath in config")
	}
	serviceSource := json.Get("serviceSource").String()
	// 固定地址和dns类型的serviceName,为控制台中创建服务时指定
	// nacos和k8s来源的serviceName,即服务注册时指定的原始名称
	serviceName := json.Get("serviceName").String()
	servicePort := json.Get("servicePort").Int()
	if serviceName == "" || servicePort == 0 {
		return errors.New("invalid service config")
	}
	switch serviceSource {
	case "k8s":
		namespace := json.Get("namespace").String()
		config.client = wrapper.NewClusterClient(wrapper.K8sCluster{
			ServiceName: serviceName,
			Namespace:   namespace,
			Port:        servicePort,
		})
		return nil
	case "nacos":
		namespace := json.Get("namespace").String()
		config.client = wrapper.NewClusterClient(wrapper.NacosCluster{
			ServiceName: serviceName,
			NamespaceID: namespace,
			Port:        servicePort,
		})
		return nil
	case "ip":
		config.client = wrapper.NewClusterClient(wrapper.StaticIpCluster{
			ServiceName: serviceName,
			Port:        servicePort,
		})
		return nil
	case "dns":
		domain := json.Get("domain").String()
		config.client = wrapper.NewClusterClient(wrapper.DnsCluster{
			ServiceName: serviceName,
			Port:        servicePort,
			Domain:      domain,
		})
		return nil
	default:
		return errors.New("unknown service source: " + serviceSource)
	}
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
	// 使用client的Get方法发起HTTP Get调用,此处省略了timeout参数,默认超时时间500毫秒
	config.client.Get(config.requestPath, nil,
		// 回调函数,将在响应异步返回时被执行
		func(statusCode int, responseHeaders http.Header, responseBody []byte) {
			// 请求没有返回200状态码,进行处理
			if statusCode != http.StatusOK {
				log.Errorf("http call failed, status: %d", statusCode)
				proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
					[]byte("http call failed"), -1)
				return
			}
			// 打印响应的HTTP状态码和应答body
			log.Infof("get status: %d, response body: %s", statusCode, responseBody)
			// 从应答头中解析token字段设置到原始请求头中
			token := responseHeaders.Get(config.tokenHeader)
			if token != "" {
				proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
			}
			// 恢复原始请求流程,继续往下处理,才能正常转发给后端服务
			proxywasm.ResumeHttpRequest()
		})
	// 需要等待异步回调完成,返回Pause状态,可以被ResumeHttpRequest恢复
	return types.ActionPause
}