All Products
Search
Document Center

Microservices Engine:Use Go to develop gateway plug-ins

Last Updated:Sep 27, 2024

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

    1. Download the installation file.

    2. 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, enter go version to query the version in use. If the version in use is displayed, the Golang program is successfully installed.

  • macOS

    1. Download the installation package.

    2. 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.

    3. 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

    1. Download the installation package.

    2. Run the following commands to install the Golang program.

      1. Install the Golang program.

        rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
      2. Configure the environment variable.

        export PATH=$PATH:/usr/local/go/bin
      3. 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

    1. Download the installation file.

    2. Decompress the installation package to the specified directory.

    3. Configure the environment variable. If the installation file is placed in the C:\tinygo directory after the decompression, you must add C:\tinygo\bin to the environment variable PATH. For example, you can run the following SET command in Command Prompt to add the configuration.

      set PATH=%PATH%;"C:\tinygo\bin";
    4. 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

    1. 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
    2. 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 variable PATH.

      export PATH=/tmp/tinygo/bin:$PATH
    3. 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.

    1. Download and install the DEB file.

      1. Download the DEB file.

        wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo_0.28.1_amd64.deb
      2. Install the DEB file.

        sudo dpkg -i tinygo_0.28.1_amd64.deb
      3. Configure the environment variable.

        export PATH=$PATH:/usr/local/bin
    2. 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

    1. Download the installation file.

    2. 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

  1. Create a project folder, such as wasm-demo-go.

  2. Run the following command in the created folder to initialize the Go project:

    go mod init wasm-demo-go
  3. Configure the proxy for downloading dependencies.

    go env -w GOPROXY=https://proxy.golang.com.cn,direct
  4. 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 if mockEnable: true is configured for the plug-in.

  • The hello: world request header is added to a request if no configuration is performed or mockEnable: false is configured for the plug-in.

For more information, see Example.

Note

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.

-

Warning

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

  1. 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.

  2. 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: {}
  3. Create another file named envoy.yaml in the folder. The envoy.yaml file contains the following content:

    Show YAML code

    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: |
                          {
                            "mockEnable": false
                          }
              - 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
  4. Run the following command to start docker compose:

    docker compose up

Feature verification

Verify the plug-in

  1. 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.1

  2. 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.2

Verify the configuration modification of the plug-in

  1. Modify the envoy.yaml file. Set the mockEnable parameter to true.4

  2. 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 that mock is enabled and the modified plug-in configuration has taken effect.3

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.

Warning

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.

  1. 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.

    image

  2. 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.

    image

    image

  3. 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
    }
  4. Compile the plug-in, upload the plug-in to the plug-in marketplace, and enable it. The following figure shows the sample configurations.

    image

  5. 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.

    image

    image.png