Desarrolle y depure localmente plugins personalizados de gateway en Go para extender la funcionalidad del API gateway y cubrir requisitos empresariales complejos.
Higress migró de TinyGo 0.29 + Go 1.20 a la compilación nativa de Wasm en Go 1.24. Go 1.24 admite de forma nativa la compilación de archivos .wasm.
Para migrar de TinyGo a Go 1.24, traslade la lógica de inicialización de su plugin desde la función main hacia la función init y actualice las dependencias en su archivo go.mod. A continuación se proporciona un ejemplo.
Ajustes de compatibilidad para migrar plugins basados en TinyGo:
1. Si llama a un servicio externo durante el procesamiento de encabezados y devuelve type.ActionPause, reemplácelo por types.HeaderStopAllIterationAndWatermark. El ejemplo "Llamada a un servicio HTTP externo" que aparece más adelante ilustra este caso.
2. Si utilizaba go-re2 (necesario porque TinyGo tenía soporte incompleto para regexp), sustitúyalo por el paquete estándar regexp de Go.
Requisitos previos
Instale Go 1.24 o una versión posterior en su computadora de desarrollo.
Golang
Instale Go 1.24 o una versión posterior desde la guía de instalación oficial .
Los plugins de Go 1.24 requieren Cloud-native API Gateway 2.1.5 o superior. Para versiones anteriores del gateway, consulte Desarrollo de plugins WASM con el lenguaje Go.
Windows
Descargue el archivo de instalación.
Ejecute el instalador. De forma predeterminada, Go se instala en
Program FilesoProgram Files (x86).Presione Win+R, introduzca
cmdy haga clic en OK para abrir el símbolo del sistema. Ejecutego versionpara comprobar la instalación.
macOS
Descargue el paquete de instalación.
Haga doble clic en el paquete para instalarlo. De forma predeterminada, Go se instala en
/usr/local/go.Abra una terminal y ejecute
go versionpara comprobar la instalación.
Linux
Descargue el paquete de instalación.
-
Ejecute los siguientes comandos para instalar Go:
-
Instale Go.
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz -
Configure la variable de entorno.
export PATH=$PATH:/usr/local/go/bin Ejecute
go versionpara comprobar la instalación.
-
Escritura del plugin
Inicialización del proyecto
-
Cree un directorio de proyecto e inicialice un módulo de Go:
mkdir wasm-demo-go && cd wasm-demo-go go mod init wasm-demo-go -
Descargue las dependencias del SDK del plugin:
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24 go get github.com/higress-group/wasm-go@main go get github.com/tidwall/gjsonSi se encuentra en China continental, configure primero un proxy:
go env -w GOPROXY=https://proxy.golang.com.cn,direct
Escritura del archivo main.go
El siguiente ejemplo agrega un encabezado de solicitud hello: world a cada solicitud entrante. Cuando mockEnable es true en la configuración del plugin, este devuelve hello world directamente en lugar de reenviar la solicitud al backend.
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"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() {}
func init() {
wrapper.SetCtx(
// Plugin name
"my-plugin",
// Custom configuration parser
wrapper.ParseConfigBy(parseConfig),
// Hook into the request header processing phase
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// Custom plugin configuration
type MyConfig struct {
mockEnable bool
}
// Parse the JSON configuration into the config struct.
// The gateway console converts YAML to JSON automatically.
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
config.mockEnable = json.Get("mockEnable").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
if config.mockEnable {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
}
return types.HeaderContinue
}
Puntos clave:
La función
main()debe estar vacía. Coloque toda la lógica de inicialización eninit().wrapper.SetCtxregistra el nombre del plugin, el analizador de configuración y los hooks de procesamiento.Devuelva
types.HeaderContinue(equivalente atypes.ActionContinue) para pasar la solicitud al siguiente filtro.
Referencia del SDK
Métodos de utilidad
El SDK del plugin proporciona los siguientes métodos de proxywasm para manipular solicitudes y respuestas:
Procesamiento de encabezados de solicitud (válido durante la fase de encabezados de solicitud)
|
Method |
Purpose |
|
|
Obtiene todos los encabezados de solicitud |
|
|
Reemplaza todos los encabezados de solicitud |
|
|
Obtiene un encabezado de solicitud específico |
|
|
Elimina un encabezado de solicitud específico |
|
|
Reemplaza un encabezado de solicitud específico |
|
|
Agrega un encabezado de solicitud |
Procesamiento del cuerpo de solicitud (válido durante la fase de cuerpo de solicitud)
|
Method |
Purpose |
|
|
Obtiene el cuerpo de la solicitud |
|
|
Anexa datos al final del cuerpo de la solicitud |
|
|
Antepone datos al inicio del cuerpo de la solicitud |
|
|
Reemplaza todo el cuerpo de la solicitud |
Procesamiento de encabezados de respuesta (válido durante la fase de encabezados de respuesta)
|
Method |
Purpose |
|
|
Obtiene todos los encabezados de respuesta del backend |
|
|
Reemplaza todos los encabezados de respuesta |
|
|
Obtiene un encabezado de respuesta específico |
|
|
Elimina un encabezado de respuesta específico |
|
|
Reemplaza un encabezado de respuesta específico |
|
|
Agrega un encabezado de respuesta |
Procesamiento del cuerpo de respuesta (válido durante la fase de cuerpo de respuesta)
|
Method |
Purpose |
|
|
Obtiene el cuerpo de la respuesta |
|
|
Anexa datos al final del cuerpo de la respuesta |
|
|
Antepone datos al inicio del cuerpo de la respuesta |
|
|
Reemplaza todo el cuerpo de la respuesta |
Llamadas HTTP y control de flujo
|
Method |
Purpose |
|
|
Envía una solicitud HTTP a un servicio externo |
|
|
Obtiene los encabezados de respuesta de una solicitud |
|
|
Obtiene el cuerpo de respuesta de una solicitud |
|
|
Obtiene los trailers de respuesta de una solicitud |
|
|
Devuelve una respuesta HTTP directamente al cliente |
|
|
Reanuda un flujo de procesamiento de solicitud pausado |
|
|
Reanuda un flujo de procesamiento de respuesta pausado |
No llame a ResumeHttpRequest ni a ResumeHttpResponse cuando el procesamiento no esté pausado. Después de llamar a SendHttpResponse, cualquier solicitud o respuesta pausada se reanuda automáticamente. Llamar nuevamente a ResumeHttpRequest o ResumeHttpResponse provoca un comportamiento indefinido.
Códigos de estado de encabezados
Cada hook de procesamiento devuelve un código de estado de encabezado que controla el flujo de la solicitud. Elija el estado adecuado según si su plugin necesita pausar, almacenar en búfer o transmitir datos:
|
Status |
Behavior |
|
|
El filtro actual finalizó. Pasa la solicitud al siguiente filtro. Equivalente a |
|
|
Retiene el encabezado pero continúa leyendo los datos del cuerpo. Utilícelo para modificar encabezados de solicitud durante la fase de procesamiento del cuerpo. Se requiere un cuerpo; si no existe, la solicitud se bloquea indefinidamente. Compruébelo con HasRequestBody(). |
|
|
Pasa el encabezado al siguiente filtro con |
|
|
Detiene toda iteración y almacena en búfer encabezados, cuerpo y trailers. Si el búfer supera el límite, el gateway devuelve 413 durante la fase de solicitud o 500 durante la fase de respuesta. Reanude con |
|
|
Igual que |
Para ver ejemplos prácticos de HeaderStopIteration y HeaderStopAllIterationAndWatermark, consulte el plugin ai-transformer y el plugin ai-quota de Higress.
Compilación del archivo Wasm
Compile el plugin en un binario Wasm:
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
Esto genera un archivo main.wasm. Utilice este archivo para depuración local o cárguelo mediante el marketplace del gateway cloud-native para desplegar un plugin personalizado.
Para desplegar mediante Higress con la definición de recurso personalizado (CRD) WasmPlugin o la interfaz de usuario de la consola, empaquete el archivo Wasm en una imagen OCI o Docker. Para más detalles, consulte Plugins personalizados.
Depuración local
Requisitos previos
Instale Docker.
Configuración del entorno de prueba
Asegúrese de que main.wasm exista en el directorio de su proyecto y cree los dos archivos siguientes.
docker-compose.yaml
version: '3.7'
services:
envoy:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
entrypoint: /usr/local/bin/envoy
# Enable debug-level logging for Wasm. Use info level in production.
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: {}
envoy.yaml
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
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.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
Inicie el entorno:
docker compose up
Verificación
Pruebe el encabezado agregado. Envíe una solicitud a través del gateway (puerto 10000) y confirme que aparece el encabezado Hello: world:
curl http://127.0.0.1:10000/get
Respuesta esperada:
{
"args": {},
"headers": {
"Accept": "*/*",
"Hello": "world",
"Host": "127.0.0.1:10000",
"Original-Host": "127.0.0.1:10000",
"Req-Start-Time": "1681269273896",
"User-Agent": "curl/7.79.1",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "172.18.0.3",
"url": "https://127.0.0.1:10000/get"
}
El encabezado Hello: world confirma que el plugin está activo.
Como comparación, una solicitud directa a httpbin (puerto 12345) no incluye este encabezado:
curl http://127.0.0.1:12345/get
Pruebe un cambio de configuración. Edite envoy.yaml y establezca mockEnable en true:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": true
}
Reinicie el entorno y envíe la misma solicitud:
curl http://127.0.0.1:10000/get
Respuesta esperada:
hello world
La respuesta simulada confirma que los cambios de configuración surten efecto correctamente.
Más ejemplos
Plugin sin configuración
Para un plugin que no requiere configuración, defina una estructura de configuración vacía y omita ParseConfigBy:
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)
func main() {}
func init() {
wrapper.SetCtx(
"hello-world",
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
return types.HeaderContinue
}
Llamada a un servicio HTTP externo
Los plugins admiten llamadas HTTP a servicios Nacos, servicios Kubernetes y servicios de dirección fija o DNS configurados en la consola del gateway. La biblioteca estándar net/http no está disponible en el entorno de ejecución Wasm. Utilice en su lugar el cliente HTTP encapsulado del SDK.
El siguiente ejemplo analiza la configuración del servicio durante el inicio y luego llama al servicio durante el procesamiento de la solicitud. Extrae un token de los encabezados de respuesta y lo inyecta en la solicitud original.
package main
import (
"errors"
"net/http"
"strings"
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"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() {}
func init() {
wrapper.SetCtx(
"http-call",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {
// HTTP client for external calls
client wrapper.HttpClient
// Path to request on the external service
requestPath string
// Response header key to extract and inject into the original request
tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig, log logs.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")
}
// Full FQDN with service type suffix.
// Examples: my-svc.dns, my-svc.static,
// service-provider.DEFAULT-GROUP.public.nacos,
// httpbin.my-ns.svc.cluster.local
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
// Default port for static IP services
servicePort = 80
}
}
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
// Send an async HTTP GET request. Default timeout is 500 ms.
err := config.client.Get(config.requestPath, nil,
// Callback runs when the response arrives
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
return
}
log.Infof("get status: %d, response body: %s", statusCode, responseBody)
// Extract the token from the response and add it to the original request
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
// Resume the paused request so it can be forwarded to the backend
proxywasm.ResumeHttpRequest()
})
if err != nil {
// If the service call fails, let the request proceed and log the error
log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
return types.ActionContinue
} else {
// Pause the request until the async callback completes
return types.HeaderStopAllIterationAndWatermark
}
}
Llamada a Redis desde un plugin
El siguiente ejemplo implementa un plugin de limitación de tasa respaldado por Redis. Rastrea las solicitudes por minuto (QPM) y devuelve HTTP 429 cuando se supera el límite.
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/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
)
func main() {}
func init() {
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 logs.Log) error {
// Full FQDN with service type suffix.
// Examples: my-redis.dns, redis.my-ns.svc.cluster.local
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80
} else {
servicePort = 6379
}
}
username := json.Get("username").String()
password := json.Get("password").String()
// Timeout in milliseconds
timeout := json.Get("timeout").Int()
if timeout == 0 {
timeout = 1000
}
qpm := json.Get("qpm").Int()
config.qpm = int(qpm)
config.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return config.client.Init(username, password, timeout)
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
now := time.Now()
minuteAligned := now.Truncate(time.Minute)
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
// If err != nil, the gateway likely cannot reach the Redis backend.
// Verify that the Redis service has not been deleted.
err := config.client.Incr(timeStamp, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("call redis error: %v", response.Error())
proxywasm.ResumeHttpRequest()
} else {
ctx.SetContext("timeStamp", timeStamp)
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm-response.Integer()))
if response.Integer() == 1 {
err := config.client.Expire(timeStamp, 60, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("call redis error: %v", response.Error())
}
proxywasm.ResumeHttpRequest()
})
if err != nil {
log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
proxywasm.ResumeHttpRequest()
}
} else {
if response.Integer() > config.qpm {
proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"callTimeLeft", "0"}}, []byte("Too many requests\n"), -1)
} else {
proxywasm.ResumeHttpRequest()
}
}
}
})
if err != nil {
// If the Redis call fails, let the request proceed and log the error
log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
return types.HeaderContinue
} else {
// Pause the request until the Redis callback completes
return types.HeaderStopAllIterationAndWatermark
}
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.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.HeaderContinue
}