You can develop gateway plug-ins to extend core features of API gateways to meet complex and specific business requirements. This topic describes how to use the Go programming language to develop a gateway plug-in. This topic also provides guidance on local development and debugging.
Preparations
Install the Golang, TinyGo, and Binaryen programs.
Golang
Install the Golang program of 1.18 or later. The installation process varies based on the operating system that you use.
Windows
Download the installation file.
Double-click the downloaded installation file to install the program. After you install the program, press Win+R. In the Run dialog box, enter
cmd
and click OK. In Command Prompt, entergo version
to query the version in use. If the version in use is displayed, the Golang program is successfully installed.
macOS
Download the installation package.
Decompress the downloaded installation package and double-click the installation file to install the program. By default, the Golang program is installed in the
/usr/local/go
directory.In Terminal, enter
go version
to query the version in use. If the version in use is displayed, the Golang program is successfully installed.
Linux
Download the installation package.
Run the following commands to install the Golang program.
Install the Golang program.
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
Configure the environment variable.
export PATH=$PATH:/usr/local/go/bin
Run the
go version
command to query the version in use. If the version in use is displayed, the Golang program is successfully installed.
TinyGo
Install the TinyGo program of 0.28.1. The installation process varies based on the operating system that you use.
Windows
Download the installation file.
Decompress the installation package to the specified directory.
Configure the environment variable. If the installation file is placed in the
C:\tinygo
directory after the decompression, you must addC:\tinygo\bin
to the environment variablePATH
. For example, you can run the followingSET
command in Command Prompt to add the configuration.set PATH=%PATH%;"C:\tinygo\bin";
In Command Prompt, run
tinygo version
to query the version in use. If the version in use is displayed, the TinyGo program is successfully installed.
macOS
Download and decompress the installation package.
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo0.28.1.darwin-amd64.tar.gz tar -zxf tinygo0.28.1.darwin-amd64.tar.gz
Configure the environment variable. If the installation file is placed in the
/tmp
directory after the decompression, you must add/tmp/tinygo/bin
to the environment variablePATH
.export PATH=/tmp/tinygo/bin:$PATH
In Terminal, run
tinygo version
to query the version in use. If the version in use is displayed, the TinyGo program is successfully installed.
Linux
In this example, Ubuntu Linux AMD64 is used. For more information about other operating systems, see the official guidelines.
Download and install the DEB file.
Download the DEB file.
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo_0.28.1_amd64.deb
Install the DEB file.
sudo dpkg -i tinygo_0.28.1_amd64.deb
Configure the environment variable.
export PATH=$PATH:/usr/local/bin
In Terminal, run
tinygo version
to query the version in use. If the version in use is displayed, the TinyGo program is successfully installed.
Binaryen
The Binaryen program is used to optimize the compilation of WebAssembly files. The installation process varies based on the operating system that you use.
Windows
Download the installation file.
Decompress the installation file. Copy the wasm-opt.exe file in the bin directory of the decompressed file to the bin directory where the TinyGo program resides.
macOS
Run the following command to install the Binaryen program.
brew install binaryen
Linux
In this example, Ubuntu Linux is used. Run the following command to install the Binaryen program.
apt-get -y install binaryen
Develop a plug-in
Step 1: Initialize a project folder
Create a project folder, such as wasm-demo-go.
Run the following command in the created folder to initialize the Go project:
go mod init wasm-demo-go
Configure the proxy for downloading dependencies.
go env -w GOPROXY=https://proxy.golang.com.cn,direct
Download the dependencies that are required to develop the plug-in.
go get github.com/higress-group/proxy-wasm-go-sdk go get github.com/alibaba/higress/plugins/wasm-go@main go get github.com/tidwall/gjson
Step 2: Write the main.go file
The following sample code is used to implement the following logic:
The
hello world
response is returned ifmockEnable: true
is configured for the plug-in.The
hello: world
request header is added to a request if no configuration is performed ormockEnable: false
is configured for the plug-in.
For more information, see Example.
In the Microservices Engine (MSE) console, the plug-in configuration is written in the YAML format. When the configuration is delivered to the plug-in, the configuration is automatically converted into the JSON format. Therefore, in the sample code, the parseConfig
function can directly parse the configuration from the JSON data.
package main
import (
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
// The name of the plug-in.
"my-plugin",
// Configure a user-defined function (UDF) to parse the plug-in configuration.
wrapper.ParseConfigBy(parseConfig),
// Configure a UDF to process request headers.
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// Configure the plug-in.
type MyConfig struct {
mockEnable bool
}
// The plug-in configuration in the MSE console in the YAML format is automatically converted into the JSON format. The UDF can parse the configuration from the JSON data.
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
// Parse the configuration and update it to the config file.
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
}
Mount points for HTTP request processing
In the preceding sample code, wrapper.ProcessRequestHeadersBy
applies the UDF onHttpRequestHeaders
to the HTTP request header processing stage for processing requests. The following table describes all the stages for HTTP request processing and the mount points for applying UDFs to different stages.
HTTP processing stage | Trigger condition | Mount point |
HTTP request header processing stage | The gateway receives the request header data from the client. | wrapper.ProcessRequestHeadersBy |
HTTP request body processing stage | The gateway receives the request body data from the client. | wrapper.ProcessRequestBodyBy |
HTTP response header processing stage | The gateway receives the response header data from the backend service. | wrapper.ProcessResponseHeadersBy |
HTTP response body processing stage | The gateway receives the response body data from the backend service. | wrapper.ProcessResponseBodyBy |
Utility methods
In the preceding sample code, the proxywasm.AddHttpRequestHeader
and proxywasm.SendHttpResponse
utility methods are provided by the plug-in SDK. The following table describes other utility methods.
Category | Method | Description | HTTP processing stage |
Request header processing | GetHttpRequestHeaders | Obtains all headers of a client request. | HTTP request header processing stage |
ReplaceHttpRequestHeaders | Replaces all headers of a client request. | HTTP request header processing stage | |
GetHttpRequestHeader | Obtains a specified header of a client request. | HTTP request header processing stage | |
RemoveHttpRequestHeader | Removes a specified header from a client request. | HTTP request header processing stage | |
ReplaceHttpRequestHeader | Replaces a specified header of a client request. | HTTP request header processing stage | |
AddHttpRequestHeader | Adds a header to a client request. | HTTP request header processing stage | |
Request body processing | GetHttpRequestBody | Obtains the body of a client request. | HTTP request body processing stage |
AppendHttpRequestBody | Appends a specified byte string to the body of a client request. | HTTP request body processing stage | |
PrependHttpRequestBody | Prepends a specified byte string to the body of a client request. | HTTP request body processing stage | |
ReplaceHttpRequestBody | Replaces the body of a client request. | HTTP request body processing stage | |
Response header processing | GetHttpResponseHeaders | Obtains all headers of a backend response. | HTTP response header processing stage |
ReplaceHttpResponseHeaders | Replaces all headers of a backend response. | HTTP response header processing stage | |
GetHttpResponseHeader | Obtains a specified header of a backend response. | HTTP response header processing stage | |
RemoveHttpResponseHeader | Removes a specified header from a backend response. | HTTP response header processing stage | |
ReplaceHttpResponseHeader | Replaces a specified header of a backend response. | HTTP response header processing stage | |
AddHttpResponseHeader | Adds a header to a backend response. | HTTP response header processing stage | |
Response body processing | GetHttpResponseBody | Obtains the body of a client request. | HTTP response body processing stage |
AppendHttpResponseBody | Appends a specified byte string to the body of a backend response. | HTTP response body processing stage | |
PrependHttpResponseBody | Prepends a specified byte string to the body of a backend response. | HTTP response body processing stage | |
ReplaceHttpResponseBody | Replaces the body of a backend response. | HTTP response body processing stage | |
HTTP calls | DispatchHttpCall | Sends an HTTP request. | - |
GetHttpCallResponseHeaders | Obtains the header of a response to a DispatchHttpCall request. | - | |
GetHttpCallResponseBody | Obtains the body of a response to a DispatchHttpCall request. | - | |
GetHttpCallResponseTrailers | Obtains the trailer of a response to a DispatchHttpCall request. | - | |
Direct response | SendHttpResponse | Returns a specified HTTP response. | - |
Process recovery | ResumeHttpRequest | Resumes the previously suspended request processing process. | - |
ResumeHttpResponse | Resumes the previously suspended response processing process. | - |
Call the ResumeHttpRequest or ResumeHttpResponse method only when the request or response is in the pause state. After you call the SendHttpResponse method, the request or response that is in the pause state automatically recovers. If you call the ResumeHttpRequest or ResumeHttpResponse method again, undefined behaviors occur.
Step 3: Compile and create a .wasm file
Run the following command to create a .wasm file:
go mod tidy
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go
A file named main.wasm is created in the current directory. The main.wasm file is used in the example for local debugging in this topic.
If you use a custom plug-in that is created on the Plug-in Marketplace page in the MSE console, you can directly upload this file.
Perform local debugging
For more information about the code and configurations that are used in this example, see Demo.
Prepare the required tool
Install the Docker program. For more information, see Install Docker Engine.
Start Docker Compose for verification
Go to the folder that is created when you develop the plug-in, such as the wasm-demo folder. Confirm that the main.wasm file is compiled and created in the folder.
Create a file named docker-compose.yaml in the folder. The docker-compose.yaml file contains the following content:
version: '3.7' services: envoy: image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v1.4.0-rc.1 entrypoint: /usr/local/bin/envoy # Take note that debug-level logging is enabled for the .wasm file. By default, info-level logging is used for official deployment. command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug depends_on: - httpbin networks: - wasmtest ports: - "10000:10000" volumes: - ./envoy.yaml:/etc/envoy/envoy.yaml - ./main.wasm:/etc/envoy/main.wasm httpbin: image: kennethreitz/httpbin:latest networks: - wasmtest ports: - "12345:80" networks: wasmtest: {}
Create another file named envoy.yaml in the folder. The envoy.yaml file contains the following content:
Run the following command to start
docker compose
:docker compose up
Feature verification
Verify the plug-in
Use a browser to access httpbin (http://127.0.0.1:12345/get). The following figure shows the header of the request that is not processed by the gateway.
Use a browser to access httpbin (http://127.0.0.1:10000/get) by using a gateway. The following figure shows the header of the request that is processed by the gateway. The header content indicates that the
hello: world
request header is added and the plug-in has taken effect.
Verify the configuration modification of the plug-in
Modify the envoy.yaml file. Set the
mockEnable
parameter totrue
.Use a browser to access httpbin (http://127.0.0.1:10000/get). The following figure shows the header of the request that is processed by the gateway. The request header
hello world
is returned, which indicates thatmock
is enabled and the modified plug-in configuration has taken effect.
Examples
Develop a plug-in with no configurations
If you do not need to configure the plug-in, you can define an empty structure.
package main
import (
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/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
}
Use a plug-in to access external services
You can use a plug-in to access only HTTP services. The service source of the HTTP services that is configured in the MSE console can be the Nacos service, Kubernetes service, fixed address, or DNS. You cannot directly use the HTTP client in the net/http
library. You must use the encapsulated HTTP client in the following example. In the example, the service type is parsed in the configuration parsing stage and the HTTP client is generated. In the request header processing stage, the service is accessed based on the configured request path, the response header is parsed, and the parsed response header is then configured in the original request header.
package main
import (
"errors"
"net/http"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/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 {
// The client that is used to initiate an HTTP request.
client wrapper.HttpClient
// The request URL.
requestPath string
// The key. The response header of the service is obtained based on the key. The obtained response header is then configured in the original request header.
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()
// The name of the service whose service source is a fixed address or DNS. The service source and service name are specified when you create the service in the MSE console.
// The name of the service whose service source is Nacos or Kubernetes. The name is specified during the service registration.
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 {
// Use the GET method of the client to initiate an HTTP request. The timeout parameter is omitted. The default timeout period is 500 milliseconds.
config.client.Get(config.requestPath, nil,
// The callback function, which is executed if the response is returned asynchronously.
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
// The message that is returned if HTTP status code 200 is not returned.
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
// The request automatically recovers after you call the SendHttpResponse method. The response is returned. You do not need to call the ResumeHttpRequest method.
return
}
// Log the HTTP status code and response body.
log.Infof("get status: %d, response body: %s", statusCode, responseBody)
// Parse the token field from the response header and configure it in the original request header.
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
// Resume the original request process. In this case, requests can be forwarded to the backend service.
proxywasm.ResumeHttpRequest()
})
// Use ResumeHttpRequest to resume the original request process only after the asynchronous callback process is complete and pause is returned.
return types.ActionPause
}
Use a plug-in to call ApsaraDB for Redis services
MSE cloud-native gateways allow you to use a plug-in to call ApsaraDB for Redis services. In the plug-in configuration parsing stage, create a Redis client based on the information of ApsaraDB for Redis services in the plug-in configuration. Then, you can call the ApsaraDB for Redis services at different request stages. For more information about the interfaces that are supported by a Redis client, see Redis client interfaces.
Do not call other Redis client interfaces except Init in the configuration parsing stage. For example, in the sample code, only the Init interface is used in the parseConfig function. Redis commands that are initiated in the configuration parsing stage will result in undefined behaviors.
The following steps describe how to throttle requests to an ApsaraDB for Redis instance by using a gateway Redis plug-in. For more information about the sample code, see Sample code.
Log on to the ApsaraDB for Redis console, create an ApsaraDB for Redis instance, and configure the password for connecting to the instance. For more information, see Overview.
Record the endpoint of the ApsaraDB for Redis instance, add an ApsaraDB for Redis service to the gateway, and set the service source to the DNS domain name. For more information, see Add a service.
Run the following code to create a Redis throttling plug-in:
package main import ( "strconv" "time" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" "github.com/tidwall/gjson" "github.com/tidwall/resp" "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" ) func main() { wrapper.SetCtx( "redis-demo", wrapper.ParseConfigBy(parseConfig), wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders), ) } type RedisCallConfig struct { client wrapper.RedisClient qpm int } func parseConfig(json gjson.Result, config *RedisCallConfig, log wrapper.Log) error { serviceName := json.Get("serviceName").String() servicePort := json.Get("servicePort").Int() domain := json.Get("domain").String() username := json.Get("username").String() password := json.Get("password").String() timeout := json.Get("timeout").Int() qpm := json.Get("qpm").Int() config.qpm = int(qpm) config.client = wrapper.NewRedisClusterClient(wrapper.DnsCluster{ ServiceName: serviceName, Port: servicePort, Domain: domain, }) return config.client.Init(username, password, timeout) } func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log wrapper.Log) types.Action { now := time.Now() minuteAligned := now.Truncate(time.Minute) timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10) config.client.Incr(timeStamp, func(status int, response resp.Value) { if status != 0 { log.Errorf("Error occured while calling redis") proxywasm.SendHttpResponse(430, nil, []byte("Error while calling redis"), -1) } else { ctx.SetContext("timeStamp", timeStamp) ctx.SetContext("CallTimeLeft", strconv.Itoa(config.qpm-response.Integer())) if response.Integer() == 1 { config.client.Expire(timeStamp, 60, func(status int, response resp.Value) { if status != 0 { log.Errorf("Error occured while calling redis") } proxywasm.ResumeHttpRequest() }) } else { if response.Integer() > config.qpm { proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"CallTimeLeft", "0"}}, []byte("Too many requests"), -1) } else { proxywasm.ResumeHttpRequest() } } } }) return types.ActionPause } func onHttpResponseHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log wrapper.Log) types.Action { if ctx.GetContext("timeStamp") != nil { proxywasm.AddHttpResponseHeader("timeStamp", ctx.GetContext("timeStamp").(string)) } if ctx.GetContext("CallTimeLeft") != nil { proxywasm.AddHttpResponseHeader("CallTimeLeft", ctx.GetContext("CallTimeLeft").(string)) } return types.ActionContinue }
Compile the plug-in, upload the plug-in to the plug-in marketplace, and enable it. The following figure shows the sample configurations.
In the MSE console, create a mock route. Then, access the IP address of the Server Load Balancer (SLB) instance of the gateway to verify the feature of the Redis throttling plug-in.