Kembangkan dan debug secara lokal plugin gerbang kustom dalam Go untuk memperluas fungsionalitas API Gateway guna memenuhi kebutuhan bisnis yang kompleks.
Higress telah bermigrasi dari TinyGo 0.29 + Go 1.20 ke kompilasi Wasm native di Go 1.24. Go 1.24 mendukung secara native kompilasi file .wasm.
Untuk bermigrasi dari TinyGo ke Go 1.24, pindahkan logika inisialisasi plugin Anda dari fungsi main ke fungsi init dan perbarui dependensi go.mod Anda. Contoh disediakan di bawah ini.
Penyesuaian kompatibilitas untuk migrasi plugin berbasis TinyGo:
1. Jika Anda memanggil layanan eksternal selama pemrosesan header dan mengembalikan type.ActionPause, gantilah dengan types.HeaderStopAllIterationAndWatermark. Contoh "Panggil layanan eksternal dari plugin" di bawah ini menunjukkan hal tersebut.
2. Jika Anda menggunakan go-re2 (diperlukan karena dukungan regexp TinyGo tidak lengkap), gantilah dengan paket regexp standar Go.
Prasyarat
Instal Go 1.24 atau versi yang lebih baru pada mesin pengembangan Anda.
Golang
Instal Go 1.24 atau versi yang lebih baru dari panduan instalasi resmi.
Plugin Go 1.24 memerlukan Cloud-native API Gateway 2.1.5 atau versi yang lebih baru. Versi gerbang yang lebih lama harus mengikuti Kembangkan plugin WASM menggunakan bahasa Go.
Windows
Unduh file instalasi.
Jalankan installer. Go terinstal ke
Program FilesatauProgram Files (x86)secara default.Tekan Win+R, masukkan
cmd, lalu klik OK untuk membuka command prompt. Jalankango versionuntuk memverifikasi instalasi.
macOS
Unduh paket instalasi.
Klik ganda paket tersebut untuk menginstal. Go terinstal ke
/usr/local/gosecara default.Buka terminal dan jalankan
go versionuntuk memverifikasi instalasi.
Linux
Unduh paket instalasi.
Jalankan perintah-perintah berikut untuk menginstal Go:
Instal Go.
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gzKonfigurasikan variabel lingkungan.
export PATH=$PATH:/usr/local/go/binJalankan
go versionuntuk memverifikasi instalasi.
Tulis plugin
Inisialisasi proyek
Buat direktori proyek dan inisialisasi modul Go:
mkdir wasm-demo-go && cd wasm-demo-go go mod init wasm-demo-goUnduh dependensi SDK 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/gjsonJika Anda berada di daratan Tiongkok, atur proxy terlebih dahulu:
go env -w GOPROXY=https://proxy.golang.com.cn,direct
Tulis file main.go
Contoh berikut menambahkan header permintaan hello: world ke setiap permintaan masuk. Ketika mockEnable bernilai true dalam konfigurasi plugin, plugin akan langsung mengembalikan hello world alih-alih meneruskan ke 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(
// Nama plugin
"my-plugin",
// Parser konfigurasi kustom
wrapper.ParseConfigBy(parseConfig),
// Hook ke fase pemrosesan header permintaan
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// Konfigurasi plugin kustom
type MyConfig struct {
mockEnable bool
}
// Uraikan konfigurasi JSON ke dalam struct config.
// Konsol gerbang secara otomatis mengonversi YAML ke JSON.
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
}Poin-poin penting:
Fungsi
main()harus kosong. Tempatkan seluruh logika inisialisasi diinit().wrapper.SetCtxmendaftarkan nama plugin, parser konfigurasi, dan hook pemrosesan.Kembalikan
types.HeaderContinue(setara dengantypes.ActionContinue) untuk meneruskan permintaan ke filter berikutnya.
Referensi SDK
Metode utilitas
SDK plugin menyediakan metode proxywasm berikut untuk manipulasi permintaan dan respons:
Pemrosesan header permintaan (berlaku selama fase header permintaan)
Metode | Tujuan |
| Ambil semua header permintaan |
| Ganti semua header permintaan |
| Ambil header permintaan tertentu |
| Hapus header permintaan tertentu |
| Ganti header permintaan tertentu |
| Tambahkan header permintaan |
Pemrosesan badan permintaan (berlaku selama fase badan permintaan)
Metode | Tujuan |
| Ambil badan permintaan |
| Tambahkan data ke akhir badan permintaan |
| Tambahkan data ke awal badan permintaan |
| Ganti seluruh badan permintaan |
Pemrosesan header respons (berlaku selama fase header respons)
Metode | Tujuan |
| Ambil semua header respons dari backend |
| Ganti semua header respons |
| Ambil header respons tertentu |
| Hapus header respons tertentu |
| Ganti header respons tertentu |
| Tambahkan header respons |
Pemrosesan badan respons (berlaku selama fase badan respons)
Metode | Tujuan |
| Ambil badan respons |
| Tambahkan data ke akhir badan respons |
| Tambahkan data ke awal badan respons |
| Ganti seluruh badan respons |
Panggilan HTTP dan kontrol aliran
Metode | Tujuan |
| Kirim permintaan HTTP ke layanan eksternal |
| Ambil header respons dari permintaan |
| Ambil badan respons dari permintaan |
| Ambil trailer respons dari permintaan |
| Kembalikan respons HTTP langsung ke klien |
| Lanjutkan aliran pemrosesan permintaan yang dijeda |
| Lanjutkan aliran pemrosesan respons yang dijeda |
Jangan panggil ResumeHttpRequest atau ResumeHttpResponse ketika pemrosesan tidak dijeda. Setelah SendHttpResponse dipanggil, permintaan atau respons yang dijeda akan dilanjutkan secara otomatis. Memanggil ResumeHttpRequest atau ResumeHttpResponse lagi menyebabkan perilaku yang tidak terdefinisi.
Kode status header
Setiap hook pemrosesan mengembalikan kode status header yang mengontrol aliran permintaan. Pilih status yang sesuai berdasarkan apakah plugin Anda perlu menjeda, mem-buffer, atau melakukan streaming data:
Status | Perilaku |
| Filter saat ini selesai. Teruskan permintaan ke filter berikutnya. Setara dengan |
| Tahan header tetapi lanjutkan membaca data badan. Gunakan ini untuk memodifikasi header permintaan selama fase pemrosesan badan. Badan diperlukan — jika tidak ada badan, permintaan akan diblokir tanpa batas. Periksa dengan |
| Teruskan header ke filter berikutnya dengan |
| Hentikan semua iterasi dan buffer header, badan, serta trailer. Jika buffer melebihi batas, gerbang mengembalikan 413 selama fase permintaan atau 500 selama fase respons. Lanjutkan dengan |
| Sama seperti |
Untuk contoh nyata penggunaan HeaderStopIteration dan HeaderStopAllIterationAndWatermark, lihat plugin Higress ai-transformer dan ai-quota.
Kompilasi file Wasm
Kompilasi plugin menjadi biner Wasm:
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./Perintah ini menghasilkan file main.wasm. Gunakan file ini untuk debugging lokal atau unggah melalui pasar gerbang cloud-native untuk men-deploy plugin kustom.
Untuk men-deploy melalui Higress menggunakan CustomResourceDefinition (CRD) WasmPlugin atau antarmuka Konsol, kemas file Wasm ke dalam citra OCI atau Docker. Untuk detailnya, lihat Plugin kustom.
Debugging lokal
Prasyarat
Instal Docker.
Siapkan lingkungan pengujian
Pastikan main.wasm ada di direktori proyek Anda, lalu buat dua file berikut.
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
# Aktifkan logging level debug untuk Wasm. Gunakan level info di produksi.
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
# Beri komentar baris berikut untuk menguji di jaringan v6
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: 80Jalankan lingkungan:
docker compose upVerifikasi
Uji header yang ditambahkan. Kirim permintaan melalui gerbang (port 10000) dan pastikan header Hello: world muncul:
curl http://127.0.0.1:10000/getRespons yang diharapkan:
{
"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"
}Header Hello: world mengonfirmasi bahwa plugin aktif.
Sebagai perbandingan, permintaan langsung ke httpbin (port 12345) tidak menyertakan header ini:
curl http://127.0.0.1:12345/getUji perubahan konfigurasi. Edit envoy.yaml dan atur mockEnable menjadi true:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": true
}Mulai ulang lingkungan dan kirim permintaan yang sama:
curl http://127.0.0.1:10000/getRespons yang diharapkan:
hello worldRespons mock ini mengonfirmasi bahwa perubahan konfigurasi berlaku dengan benar.
Contoh lainnya
Plugin tanpa konfigurasi
Untuk plugin yang tidak memerlukan konfigurasi, definisikan struct config kosong dan hilangkan 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
}Panggil layanan HTTP eksternal
Plugin mendukung panggilan HTTP ke layanan Nacos, layanan Kubernetes, serta layanan alamat tetap atau DNS yang dikonfigurasi di Konsol gerbang. Pustaka net/http standar tidak tersedia di runtime Wasm. Gunakan klien HTTP terenkapsulasi dari SDK sebagai gantinya.
Contoh berikut mengurai konfigurasi layanan selama startup, lalu memanggil layanan tersebut selama pemrosesan permintaan. Plugin mengekstrak token dari header respons dan menyuntikkannya ke permintaan asli.
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 {
// Klien HTTP untuk panggilan eksternal
client wrapper.HttpClient
// Path yang diminta pada layanan eksternal
requestPath string
// Kunci header respons untuk diekstrak dan disuntikkan ke permintaan asli
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")
}
// FQDN lengkap dengan akhiran tipe layanan.
// Contoh: 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") {
// Port default untuk layanan IP statis
servicePort = 80
}
}
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
// Kirim permintaan HTTP GET async. Timeout default adalah 500 ms.
err := config.client.Get(config.requestPath, nil,
// Callback dijalankan saat respons tiba
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)
// Ekstrak token dari respons dan tambahkan ke permintaan asli
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
// Lanjutkan permintaan yang dijeda agar dapat diteruskan ke backend
proxywasm.ResumeHttpRequest()
})
if err != nil {
// Jika panggilan layanan gagal, biarkan permintaan dilanjutkan dan catat error
log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
return types.ActionContinue
} else {
// Jeda permintaan hingga callback async selesai
return types.HeaderStopAllIterationAndWatermark
}
}Panggil Redis dari plugin
Contoh berikut mengimplementasikan plugin pembatasan laju berbasis Redis. Plugin ini melacak permintaan per menit (QPM) dan mengembalikan HTTP 429 ketika batas terlampaui.
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 {
// FQDN lengkap dengan akhiran tipe layanan.
// Contoh: 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 dalam milidetik
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)
// Jika err != nil, kemungkinan besar gerbang tidak dapat menjangkau backend Redis.
// Pastikan layanan Redis belum dihapus.
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 {
// Jika panggilan Redis gagal, biarkan permintaan dilanjutkan dan catat error
log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
return types.HeaderContinue
} else {
// Jeda permintaan hingga callback Redis selesai
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
}