Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

super basic wasm filter implementation using wazero #2947

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ const (
OpaServeResponseName = "opaServeResponse"
OpaServeResponseWithReqBodyName = "opaServeResponseWithReqBody"
TLSName = "tlsPassClientCertificates"
WASMName = "wasm"

// Undocumented filters
HealthCheckName = "healthcheck"
Expand Down
15 changes: 15 additions & 0 deletions filters/wasm/testdata/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

//export request
func request(x, y uint32) uint32 {
return x + y
}

//export response
func response(x, y uint32) uint32 {
return x - y
}

// main is required for the `wasi` target, even if it isn't used.
// See https://wazero.io/languages/tinygo/#why-do-i-have-to-define-main
func main() {}
Binary file added filters/wasm/testdata/add.wasm
Binary file not shown.
217 changes: 217 additions & 0 deletions filters/wasm/wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package wasm

import (
"context"
"fmt"
"net/url"
"os"

log "github.com/sirupsen/logrus"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
"github.com/zalando/skipper/filters"
)

type WASMOpts struct {
Typ string
CacheDir string
}

const (
memoryLimitPages uint32 = 8 // 8*2^16
)

type cache int

type compilationCache cache

const (
none cache = iota + 1
inmemory
filesystem
)

type wasmSpec struct {
typ cache
cacheDir string
}

// TODO(sszuecs): think about:
//
// 1) If we want to provide internal Go functions to support our wasm
// modules, we can use
// https://pkg.go.dev/github.com/tetratelabs/wazero#HostFunctionBuilder,
// such that WASM binary can import and use these functions.
// see also https://pkg.go.dev/github.com/tetratelabs/wazero#HostModuleBuilder
type wasm struct {
code []byte
runtime wazero.Runtime
mod api.Module
request api.Function
response api.Function
}

func NewWASM(o WASMOpts) filters.Spec {
typ := none
switch o.Typ {
case "none":
typ = none
case "in-memory":
typ = inmemory
case "fs":
typ = filesystem
default:
log.Errorf("Failed to find compilation cache type %q, available values 'none', 'in-memory' and 'fs'", typ)
}

return &wasmSpec{
typ: typ,
cacheDir: o.CacheDir,
}
}

// Name implements filters.Spec.
func (*wasmSpec) Name() string {
return filters.WASMName
}

// CreateFilter implements filters.Spec.
func (ws *wasmSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
if len(args) != 1 {
return nil, filters.ErrInvalidFilterParameters
}

src, ok := args[0].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}
u, err := url.Parse(src)
if err != nil {
return nil, filters.ErrInvalidFilterParameters
}

var code []byte

switch u.Scheme {
case "file":
code, err = os.ReadFile(u.Path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make this lazy and cacheable and cleanup on filter close

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, as I wrote in the PR title super basic. I used https://github.com/tetratelabs/wazero/tree/main/examples/basic to test the super basic example.

if err != nil {
return nil, fmt.Errorf("failed to load file %q: %v", u.Path, filters.ErrInvalidFilterParameters)
}
case "https":
panic("not implemented")
default:
return nil, filters.ErrInvalidFilterParameters
}

ctx := context.Background()

var r wazero.Runtime
switch ws.typ {
// in general, we likely do not need compilation
// cache, but likely we want to use Pre-/PostProcessor
// to not recreate the filter and check in
// CreateFilter to not compile the WASM code again and
// again
case none:
// we could try to use NewRuntimeConfigCompiler for
// GOARCH specific asm for optimal performance as
// stated in
// https://pkg.go.dev/github.com/tetratelabs/wazero#NewRuntimeConfigCompiler
config := wazero.NewRuntimeConfig().WithMemoryLimitPages(memoryLimitPages)
r = wazero.NewRuntimeWithConfig(ctx, config)

case inmemory:
// TODO(sszuecs): unclear if we hit the bug described in https://pkg.go.dev/github.com/tetratelabs/wazero#RuntimeConfig for WithCompilationCache():

// Cached files are keyed on the version of wazero. This is obtained from go.mod of your application,
// and we use it to verify the compatibility of caches against the currently-running wazero.
// However, if you use this in tests of a package not named as `main`, then wazero cannot obtain the correct
// version of wazero due to the known issue of debug.BuildInfo function: https://github.com/golang/go/issues/33976.
// As a consequence, your cache won't contain the correct version information and always be treated as `dev` version.
// To avoid this issue, you can pass -ldflags "-X github.com/tetratelabs/wazero/internal/version.version=foo" when running tests.

cache := wazero.NewCompilationCache()
config := wazero.NewRuntimeConfig().WithCompilationCache(cache)
config = config.WithMemoryLimitPages(memoryLimitPages)
r = wazero.NewRuntimeWithConfig(ctx, config)

case filesystem:
cache, err := wazero.NewCompilationCacheWithDir(ws.cacheDir)
if err != nil {
return nil, fmt.Errorf("failed to create compilation cache dir with %q as directory: %w", ws.cacheDir, filters.ErrInvalidFilterParameters)
}

config := wazero.NewRuntimeConfig().WithCompilationCache(cache)
config = config.WithMemoryLimitPages(memoryLimitPages)
r = wazero.NewRuntimeWithConfig(ctx, config)

default:
return nil, fmt.Errorf("failed to create wazero runtime typ %q: %w", ws.typ, filters.ErrInvalidFilterParameters)
}

// Instantiate WASI, which implements host functions needed for TinyGo to
// implement `panic`.
// see also https://github.com/tetratelabs/wazero/blob/main/imports/README.md
// and https://wazero.io/languages/
//
// we do not need the closer because of https://pkg.go.dev/github.com/tetratelabs/[email protected]/imports/wasi_snapshot_preview1#hdr-Notes
// "Closing the wazero.Runtime has the same effect as closing the result."
_, err = wasi_snapshot_preview1.Instantiate(ctx, r)
if err != nil {
return nil, fmt.Errorf("failed to wasi_snapshot_preview1: %w: %w", err, filters.ErrInvalidFilterParameters)
}

// TODO(sszuecs): create modules to be used from user wasm code
// cmod, err := r.CompileModule(ctx, []byte(""))
// if err != nil {
// return nil, fmt.Errorf("failed to compile module: %w: %w", err, filters.ErrInvalidFilterParameters)
// }
// r.InstantiateModule(ctx)
//
// Instantiate the guest Wasm into the same runtime. It exports the `add`
// function, implemented in WebAssembly.
// mod, err := r.Instantiate(ctx, cmod, moduleConfig)
// if err != nil {
// return nil, fmt.Errorf("failed to instantiate module: %w: %w", err, filters.ErrInvalidFilterParameters)
// }

mod, err := r.Instantiate(ctx, code)
if err != nil {
return nil, fmt.Errorf("failed to instantiate module: %w: %w", err, filters.ErrInvalidFilterParameters)
}
request := mod.ExportedFunction("request")
response := mod.ExportedFunction("response")

return &wasm{
code: code,
runtime: r,
mod: mod,
request: request,
response: response,
}, nil
}

func (w *wasm) Request(ctx filters.FilterContext) {

result, err := w.request.Call(ctx.Request().Context(), 2, 3)
if err != nil {
log.Errorf("failed to call add: %v", err)
}
log.Infof("request result: %v", result)

}

func (w *wasm) Response(ctx filters.FilterContext) {
result, err := w.response.Call(context.Background(), 3, 2)
if err != nil {
log.Errorf("failed to call add: %v", err)
}
log.Infof("response result: %v", result)

}

func (w *wasm) Close() error {
return w.runtime.Close(context.Background())
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ require (
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/streadway/quantile v0.0.0-20220407130108-4246515d968d // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/tetratelabs/wazero v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,8 @@ github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BG
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.27.0 h1:IeIrJN4twonTDuMuBNQdKZ+K97yd7VrmNGu+lDpYcDk=
github.com/testcontainers/testcontainers-go v0.27.0/go.mod h1:+HgYZcd17GshBUZv9b+jKFJ198heWPQq3KQIp2+N+7U=
github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g=
github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
Expand Down
8 changes: 8 additions & 0 deletions skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
ratelimitfilters "github.com/zalando/skipper/filters/ratelimit"
"github.com/zalando/skipper/filters/shedder"
teefilters "github.com/zalando/skipper/filters/tee"
"github.com/zalando/skipper/filters/wasm"
"github.com/zalando/skipper/loadbalancer"
"github.com/zalando/skipper/logging"
"github.com/zalando/skipper/metrics"
Expand Down Expand Up @@ -1524,6 +1525,13 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
return err
}

o.CustomFilters = append(o.CustomFilters,
wasm.NewWASM(wasm.WASMOpts{
Typ: "none",
CacheDir: "",
}),
)

// tee filters override with initialized tracer
o.CustomFilters = append(o.CustomFilters,
// tee()
Expand Down