Desenvolva e depure localmente plugins de gateway personalizados em Go para estender a funcionalidade do API gateway e atender a requisitos de negócios complexos.
O Higress migrou do TinyGo 0.29 + Go 1.20 para compilação Wasm nativa no Go 1.24. O Go 1.24 oferece suporte nativo à compilação de arquivos .wasm.
Para migrar do TinyGo para o Go 1.24, mova a lógica de inicialização do plugin da função main para a função init e atualize as dependências no arquivo go.mod. Confira um exemplo abaixo.
Ajustes de compatibilidade para migrar plugins baseados em TinyGo:
1. Se você chamar um service externo durante o processamento de cabeçalhos e retornar type.ActionPause, substitua por types.HeaderStopAllIterationAndWatermark. O exemplo "Chamar um service HTTP externo", apresentado mais adiante, demonstra esse procedimento.
2. Caso tenha utilizado go-re2 (necessário porque o TinyGo tinha suporte incompleto ao pacote regexp), substitua-o pelo pacote padrão regexp do Go.
Pré-requisitos
Instale o Go 1.24 ou superior na sua máquina de desenvolvimento.
Golang
Instale o Go 1.24 ou superior seguindo o guia de instalação oficial.
Plugins desenvolvidos com Go 1.24 exigem o Cloud-native API Gateway versão 2.1.5 ou superior. Para versões anteriores do gateway, consulte Desenvolver plugins WASM usando a linguagem Go.
Windows
Baixe o arquivo de instalação.
Execute o instalador. Por padrão, o Go é instalado em
Program FilesouProgram Files (x86).Pressione Win+R, insira
cmde clique em OK para abrir o prompt de comando. Executego versionpara verificar a instalação.
macOS
Baixe o pacote de instalação.
Clique duas vezes no pacote para instalar. O Go é instalado em
/usr/local/gopor padrão.Abra um terminal e execute
go versionpara verificar a instalação.
Linux
Baixe o pacote de instalação.
-
Execute os comandos a seguir para instalar o Go:
-
Instale o Go.
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz -
Configure a variável de ambiente.
export PATH=$PATH:/usr/local/go/bin Execute
go versionpara verificar a instalação.
-
Escrever o plugin
Inicializar o projeto
-
Crie um diretório para o projeto e inicialize um módulo Go:
mkdir wasm-demo-go && cd wasm-demo-go go mod init wasm-demo-go -
Baixe as dependências do sdk do 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/gjsonSe estiver na China continental, configure um proxy antes:
go env -w GOPROXY=https://proxy.golang.com.cn,direct
Escrever o arquivo main.go
O exemplo a seguir adiciona um cabeçalho de requisição hello: world a todas as requisições recebidas. Quando mockEnable está definido como true na configuração do plugin, ele retorna hello world diretamente, sem encaminhar a requisição ao 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
}
Pontos importantes:
A função
main()deve estar vazia. Coloque toda a lógica de inicialização eminit().wrapper.SetCtxregistra o nome do plugin, o analisador de configuração e os hooks de processamento.Retorne
types.HeaderContinue(equivalente atypes.ActionContinue) para passar a requisição ao próximo filtro.
Referência do sdk
Métodos utilitários
O sdk do plugin fornece os seguintes métodos proxywasm para manipulação de requisições e respostas:
Processamento de cabeçalhos de requisição (efetivo durante a fase de cabeçalho da requisição)
|
Método |
Finalidade |
|
|
Obtém todos os cabeçalhos da requisição |
|
|
Substitui todos os cabeçalhos da requisição |
|
|
Obtém um cabeçalho específico da requisição |
|
|
Remove um cabeçalho específico da requisição |
|
|
Substitui um cabeçalho específico da requisição |
|
|
Adiciona um cabeçalho à requisição |
Processamento do corpo da requisição (efetivo durante a fase de corpo da requisição)
|
Método |
Finalidade |
|
|
Obtém o corpo da requisição |
|
|
Acrescenta dados ao final do corpo da requisição |
|
|
Insere dados no início do corpo da requisição |
|
|
Substitui todo o corpo da requisição |
Processamento de cabeçalhos de resposta (efetivo durante a fase de cabeçalho da resposta)
|
Método |
Finalidade |
|
|
Obtém todos os cabeçalhos de resposta do backend |
|
|
Substitui todos os cabeçalhos de resposta |
|
|
Obtém um cabeçalho específico da resposta |
|
|
Remove um cabeçalho específico da resposta |
|
|
Substitui um cabeçalho específico da resposta |
|
|
Adiciona um cabeçalho à resposta |
Processamento do corpo da resposta (efetivo durante a fase de corpo da resposta)
|
Método |
Finalidade |
|
|
Obtém o corpo da resposta |
|
|
Acrescenta dados ao final do corpo da resposta |
|
|
Insere dados no início do corpo da resposta |
|
|
Substitui todo o corpo da resposta |
Chamadas HTTP e controle de fluxo
|
Método |
Finalidade |
|
|
Envia uma requisição HTTP para um service externo |
|
|
Obtém os cabeçalhos de resposta de uma requisição |
|
|
Obtém o corpo da resposta de uma requisição |
|
|
Obtém os trailers de resposta de uma requisição |
|
|
Retorna uma resposta HTTP diretamente ao cliente |
|
|
Retoma um fluxo de processamento de requisição pausado |
|
|
Retoma um fluxo de processamento de resposta pausado |
Não chame ResumeHttpRequest ou ResumeHttpResponse quando o processamento não estiver pausado. Após a chamada de SendHttpResponse, qualquer requisição ou resposta pausada é retomada automaticamente. Chamar ResumeHttpRequest ou ResumeHttpResponse novamente causa comportamento indefinido.
Códigos de status de cabeçalho
Cada hook de processamento retorna um código de status de cabeçalho que controla o fluxo da requisição. Escolha o status apropriado dependendo se o seu plugin precisa pausar, armazenar em buffer ou transmitir dados:
|
Status |
Comportamento |
|
|
O filtro atual foi concluído. Passa a requisição para o próximo filtro. Equivalente a |
|
|
Retém o cabeçalho, mas continua lendo os dados do corpo. Use esta opção para modificar cabeçalhos de requisição durante a fase de processamento do corpo. Um corpo é obrigatório — se não houver corpo, a requisição fica bloqueada indefinidamente. Verifique com HasRequestBody(). |
|
|
Passa o cabeçalho para o próximo filtro com |
|
|
Interrompe toda iteração e armazena em buffer cabeçalhos, corpo e trailers. Se o buffer exceder o limite, o gateway retorna 413 durante a fase de requisição ou 500 durante a fase de resposta. Retome com |
|
|
Igual a |
Para exemplos práticos de uso de HeaderStopIteration e HeaderStopAllIterationAndWatermark, consulte os plugins Higress ai-transformer e ai-quota.
Compilar o arquivo Wasm
Compile o plugin em um binário Wasm:
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
Esse processo gera um arquivo main.wasm. Utilize esse arquivo para depuração local ou faça upload dele pelo marketplace do gateway cloud-native para implantar um plugin personalizado.
Para implantar via Higress usando a CustomResourceDefinition (CRD) WasmPlugin ou a interface do console, empacote o arquivo Wasm em uma imagem OCI ou docker. Para mais detalhes, consulte Plugins personalizados.
Depuração local
Pré-requisitos
Instale o docker.
Configurar o ambiente de teste
Certifique-se de que o arquivo main.wasm exista no diretório do seu projeto e crie os dois arquivos a seguir.
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 o ambiente:
docker compose up
Verificar
Testar o cabeçalho adicionado. Envie uma requisição através do gateway (porta 10000) e confirme se o cabeçalho Hello: world aparece:
curl http://127.0.0.1:10000/get
Resposta 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"
}
A presença do cabeçalho Hello: world confirma que o plugin está ativo.
Para comparação, uma requisição direta ao httpbin (porta 12345) não inclui esse cabeçalho:
curl http://127.0.0.1:12345/get
Testar uma alteração de configuração. Edite o arquivo envoy.yaml e defina mockEnable como true:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": true
}
Reinicie o ambiente e envie a mesma requisição:
curl http://127.0.0.1:10000/get
Resposta esperada:
hello world
A resposta simulada confirma que as alterações de configuração entraram em vigor corretamente.
Mais exemplos
Plugin sem configuração
Para um plugin que não exige configuração, defina uma struct de configuração vazia e 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
}
Chamar um service HTTP externo
Os plugins suportam chamadas HTTP para services Nacos, services Kubernetes e services de endereço fixo ou DNS configurados no console do gateway. A biblioteca padrão net/http não está disponível no runtime Wasm. Em vez disso, utilize o cliente HTTP encapsulado do sdk.
O exemplo a seguir analisa a configuração do service durante a inicialização e depois chama o service durante o processamento da requisição. Ele extrai um token dos cabeçalhos de resposta e o injeta na requisição 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
}
}
Chamar Redis a partir de um plugin
O exemplo a seguir implementa um plugin de limitação de taxa com suporte a Redis. Ele rastreia requisições por minuto (QPM) e retorna HTTP 429 quando o limite é excedido.
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
}