Karmaşık iş gereksinimleri için API ağ geçidi işlevselliğini genişletmek üzere Go dilinde özel ağ geçidi eklentileri geliştirin ve yerel olarak hata ayıklayın.
Higress, TinyGo 0.29 + Go 1.20 sürümünden Go 1.24'teki yerel Wasm derlemesine geçiş yapmıştır. Go 1.24, .wasm dosyalarını derlemeyi yerel olarak destekler.
TinyGo'dan Go 1.24'e geçiş yapmak için eklenti başlatma mantığını main işlevinden init işlevine taşıyın ve go.mod bağımlılıklarınızı güncelleyin. Aşağıda bir örnek verilmiştir.
TinyGo tabanlı eklentilerin geçişi için uyumluluk ayarlamaları:
1. Üst bilgi işleme sırasında harici bir hizmet çağırıyorsanız ve type.ActionPause döndürüyorsanız, bunun yerine types.HeaderStopAllIterationAndWatermark kullanın. Aşağıdaki "Eklentiden harici bir hizmet çağırma" örneği bunu göstermektedir.
2. go-re2 kullandıysanız (TinyGo'nun eksik regexp desteği nedeniyle gerekliydi), bunun yerine standart Go regexp paketini kullanın.
Ön koşullar
Geliştirme makinenize Go 1.24 veya sonraki bir sürümü yükleyin.
Golang
Resmi kurulum kılavuzu üzerinden Go 1.24 veya sonraki bir sürümü yükleyin.
Go 1.24 eklentileri Cloud-native API Gateway 2.1.5 veya sonraki bir sürümünü gerektirir. Daha eski ağ geçidi sürümleri için Go dilini kullanarak WASM eklentileri geliştirme bölümüne bakın.
Windows
Kurulum dosyasını indirin.
Yükleyiciyi çalıştırın. Go varsayılan olarak
Program FilesveyaProgram Files (x86)dizinine yüklenir.Win+R tuşlarına basın,
cmdgirin ve komut istemini açmak için OK öğesine tıklayın. Kurulumu doğrulamak içingo versionkomutunu çalıştırın.
macOS
Kurulum paketini indirin.
Pakete çift tıklayarak kurulumu yapın. Go varsayılan olarak
/usr/local/godizinine yüklenir.Bir terminal açın ve kurulumu doğrulamak için
go versionkomutunu çalıştırın.
Linux
Kurulum paketini indirin.
-
Go'yu yüklemek için şu komutları çalıştırın:
-
Go'yu yükleyin.
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz -
Ortam değişkenini yapılandırın.
export PATH=$PATH:/usr/local/go/bin Kurulumu doğrulamak için
go versionkomutunu çalıştırın.
-
Eklentiyi yazma
Projeyi başlatma
-
Bir proje dizini oluşturun ve bir Go modülü başlatın:
mkdir wasm-demo-go && cd wasm-demo-go go mod init wasm-demo-go -
Eklenti SDK bağımlılıklarını indirin:
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/gjsonÇin anakarasındaysanız önce bir proxy ayarlayın:
go env -w GOPROXY=https://proxy.golang.com.cn,direct
main.go dosyasını yazma
Aşağıdaki örnek, gelen her isteğe bir hello: world istek üst bilgisi ekler. Eklenti yapılandırmasında mockEnable değeri true olduğunda, eklenti arka uca iletmek yerine doğrudan hello world yanıtını döndürür.
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
}
Temel noktalar:
main()işlevi boş olmalıdır. Tüm başlatma mantığınıinit()içine yerleştirin.wrapper.SetCtxeklenti adını, yapılandırma ayrıştırıcısını ve işleme kancalarını kaydeder.İsteği sonraki filtreye aktarmak için
types.HeaderContinue(types.ActionContinueile eşdeğer) döndürün.
SDK referansı
Yardımcı yöntemler
Eklenti SDK'sı, istek ve yanıt manipülasyonu için aşağıdaki proxywasm yöntemlerini sağlar:
İstek üst bilgisi işleme (istek üst bilgisi aşamasında etkilidir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
İstek gövdesi işleme (istek gövdesi aşamasında etkilidir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Yanıt üst bilgisi işleme (yanıt üst bilgisi aşamasında etkilidir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Yanıt gövdesi işleme (yanıt gövdesi aşamasında etkilidir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HTTP çağrıları ve akış kontrolü
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
İşleme durdurulmadığında ResumeHttpRequest veya ResumeHttpResponse çağrısı yapmayın. SendHttpResponse çağrıldıktan sonra, durdurulmuş herhangi bir istek veya yanıt otomatik olarak devam eder. Tekrar ResumeHttpRequest veya ResumeHttpResponse çağrısı yapmak tanımsız davranışa neden olur.
Üst bilgi durum kodları
Her işleme kancası, istek akışını kontrol eden bir üst bilgi durum kodu döndürür. Eklentinizin verileri durdurması, tamponlaması veya akış olarak göndermesi gerekip gerekmediğine bağlı olarak uygun durumu seçin:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HeaderStopIteration ve HeaderStopAllIterationAndWatermark kullanımının gerçek dünya örnekleri için Higress ai-transformer eklentisine ve ai-quota eklentisine bakın.
Wasm dosyasını derleme
Eklentiyi bir Wasm ikili dosyasına derleyin:
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./
Bu işlem bir main.wasm dosyası üretir. Bu dosyayı yerel hata ayıklama için kullanın veya özel bir eklenti dağıtmak üzere cloud-native ağ geçidi pazar yeri aracılığıyla yükleyin.
WasmPlugin CustomResourceDefinition (CRD) veya konsol kullanıcı arayüzü kullanarak Higress üzerinden dağıtmak için Wasm dosyasını bir OCI veya Docker görüntüsüne paketleyin. Ayrıntılar için Özel eklentiler bölümüne bakın.
Yerel hata ayıklama
Ön koşullar
Docker yükleyin.
Test ortamını hazırlama
Proje dizininizde main.wasm dosyasının bulunduğundan emin olun, ardından aşağıdaki iki dosyayı oluşturun.
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
Ortamı başlatın:
docker compose up
Doğrulama
Eklenen üst bilgiyi test edin. Ağ geçidi üzerinden (10000 bağlantı noktası) bir istek gönderin ve Hello: world üst bilgisinin göründüğünü onaylayın:
curl http://127.0.0.1:10000/get
Beklenen yanıt:
{
"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"
}
Hello: world üst bilgisi, eklentinin aktif olduğunu onaylar.
Karşılaştırma amacıyla, httpbin'e doğrudan yapılan bir istek (12345 bağlantı noktası) bu üst bilgiyi içermez:
curl http://127.0.0.1:12345/get
Bir yapılandırma değişikliğini test edin. envoy.yaml dosyasını düzenleyin ve mockEnable değerini true olarak ayarlayın:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": true
}
Ortamı yeniden başlatın ve aynı isteği gönderin:
curl http://127.0.0.1:10000/get
Beklenen yanıt:
hello world
Mock yanıtı, yapılandırma değişikliklerinin doğru şekilde etkili olduğunu onaylar.
Daha fazla örnek
Yapılandırma gerektirmeyen eklenti
Yapılandırma gerektirmeyen bir eklenti için boş bir yapılandırma yapısı tanımlayın ve ParseConfigBy öğesini atlayın:
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
}
Harici bir HTTP hizmetini çağırma
Eklentiler, Nacos hizmetlerine, Kubernetes hizmetlerine ve ağ geçidi konsolunda yapılandırılan sabit adresli veya DNS hizmetlerine HTTP çağrıları destekler. Standart net/http kitaplığı Wasm çalışma zamanında kullanılamaz. Bunun yerine SDK'nın kapsüllenmiş HTTP istemcisini kullanın.
Aşağıdaki örnek, başlatma sırasında hizmet yapılandırmasını ayrıştırır, ardından istek işleme sırasında hizmeti çağırır. Yanıt üst bilgilerinden bir belirteç çıkarır ve bunu orijinal isteğe enjekte eder.
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
}
}
Eklentiden Redis çağırma
Aşağıdaki örnek, Redis tarafından desteklenen bir hız sınırlama eklentisi uygular. Dakika başına istek sayısını (QPM) izler ve limit aşıldığında HTTP 429 döndürür.
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
}