Enhance wasm plugins

Co-authored-by: Michael <[michael.matur@gmail.com](mailto:michael.matur@gmail.com)>
This commit is contained in:
Julien Salleyron 2024-06-25 09:58:04 +02:00 committed by GitHub
parent 983940ae60
commit e7d1a98c5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 329 additions and 26 deletions

View file

@ -52,7 +52,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[
switch manifest.Type {
case typeMiddleware:
middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName)
middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName, desc.Settings)
if err != nil {
return nil, err
}
@ -87,7 +87,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[
switch manifest.Type {
case typeMiddleware:
middleware, err := newMiddlewareBuilder(logCtx, localGoPath, manifest, desc.ModuleName)
middleware, err := newMiddlewareBuilder(logCtx, localGoPath, manifest, desc.ModuleName, desc.Settings)
if err != nil {
return nil, err
}
@ -128,7 +128,7 @@ func (b Builder) Build(pName string, config map[string]interface{}, middlewareNa
return nil, fmt.Errorf("unknown plugin type: %s", pName)
}
func newMiddlewareBuilder(ctx context.Context, goPath string, manifest *Manifest, moduleName string) (middlewareBuilder, error) {
func newMiddlewareBuilder(ctx context.Context, goPath string, manifest *Manifest, moduleName string, settings Settings) (middlewareBuilder, error) {
switch manifest.Runtime {
case runtimeWasm:
wasmPath, err := getWasmPath(manifest)
@ -136,7 +136,7 @@ func newMiddlewareBuilder(ctx context.Context, goPath string, manifest *Manifest
return nil, fmt.Errorf("wasm path: %w", err)
}
return newWasmMiddlewareBuilder(goPath, moduleName, wasmPath), nil
return newWasmMiddlewareBuilder(goPath, moduleName, wasmPath, settings)
case runtimeYaegi, "":
i, err := newInterpreter(ctx, goPath, manifest.Import)

View file

@ -8,20 +8,38 @@ import (
"os"
"path/filepath"
"reflect"
"strings"
"github.com/http-wasm/http-wasm-host-go/handler"
wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp"
"github.com/juliens/wasm-goexport/host"
"github.com/tetratelabs/wazero"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
)
type wasmMiddlewareBuilder struct {
path string
path string
cache wazero.CompilationCache
settings Settings
}
func newWasmMiddlewareBuilder(goPath string, moduleName, wasmPath string) *wasmMiddlewareBuilder {
return &wasmMiddlewareBuilder{path: filepath.Join(goPath, "src", moduleName, wasmPath)}
func newWasmMiddlewareBuilder(goPath, moduleName, wasmPath string, settings Settings) (*wasmMiddlewareBuilder, error) {
ctx := context.Background()
path := filepath.Join(goPath, "src", moduleName, wasmPath)
cache := wazero.NewCompilationCache()
code, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("loading Wasm binary: %w", err)
}
rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
if _, err = rt.CompileModule(ctx, code); err != nil {
return nil, fmt.Errorf("compiling guest module: %w", err)
}
return &wasmMiddlewareBuilder{path: path, cache: cache, settings: settings}, nil
}
func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
@ -33,15 +51,64 @@ func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, midd
}
func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
h, applyCtx, err := b.buildMiddleware(ctx, next, cfg, middlewareName)
if err != nil {
return nil, fmt.Errorf("building Wasm middleware: %w", err)
}
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
h.ServeHTTP(rw, req.WithContext(applyCtx(req.Context())))
}), nil
}
func (b *wasmMiddlewareBuilder) buildMiddleware(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, func(ctx context.Context) context.Context, error) {
code, err := os.ReadFile(b.path)
if err != nil {
return nil, fmt.Errorf("loading Wasm binary: %w", err)
return nil, nil, fmt.Errorf("loading binary: %w", err)
}
rt := host.NewRuntime(wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(b.cache)))
guestModule, err := rt.CompileModule(ctx, code)
if err != nil {
return nil, nil, fmt.Errorf("compiling guest module: %w", err)
}
applyCtx, err := InstantiateHost(ctx, rt, guestModule, b.settings)
if err != nil {
return nil, nil, fmt.Errorf("instantiating host module: %w", err)
}
logger := middlewares.GetLogger(ctx, middlewareName, "wasm")
config := wazero.NewModuleConfig().WithSysWalltime()
for _, env := range b.settings.Envs {
config.WithEnv(env, os.Getenv(env))
}
if len(b.settings.Mounts) > 0 {
fsConfig := wazero.NewFSConfig()
for _, mount := range b.settings.Mounts {
withDir := fsConfig.WithDirMount
prefix, readOnly := strings.CutSuffix(mount, ":ro")
if readOnly {
withDir = fsConfig.WithReadOnlyDirMount
}
parts := strings.Split(prefix, ":")
switch {
case len(parts) == 1:
withDir(parts[0], parts[0])
case len(parts) == 2:
withDir(parts[0], parts[1])
default:
return nil, nil, fmt.Errorf("invalid directory %q", mount)
}
}
config.WithFSConfig(fsConfig)
}
opts := []handler.Option{
handler.ModuleConfig(wazero.NewModuleConfig().WithSysWalltime()),
handler.ModuleConfig(config),
handler.Logger(logs.NewWasmLogger(logger)),
}
@ -49,23 +116,27 @@ func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler
if i != nil {
config, ok := i.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("could not type assert config: %T", i)
return nil, nil, fmt.Errorf("could not type assert config: %T", i)
}
data, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("marshaling config: %w", err)
return nil, nil, fmt.Errorf("marshaling config: %w", err)
}
opts = append(opts, handler.GuestConfig(data))
}
mw, err := wasm.NewMiddleware(context.Background(), code, opts...)
opts = append(opts, handler.Runtime(func(ctx context.Context) (wazero.Runtime, error) {
return rt, nil
}))
mw, err := wasm.NewMiddleware(applyCtx(ctx), code, opts...)
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("creating middleware: %w", err)
}
return mw.NewHandler(ctx, next), nil
return mw.NewHandler(ctx, next), applyCtx, nil
}
// WasmMiddleware is an HTTP handler plugin wrapper.

View file

@ -10,6 +10,11 @@ const (
typeProvider = "provider"
)
type Settings struct {
Envs []string `description:"Environment variables to forward to the wasm guest." json:"envs,omitempty" toml:"envs,omitempty" yaml:"envs,omitempty"`
Mounts []string `description:"Directory to mount to the wasm guest." json:"mounts,omitempty" toml:"mounts,omitempty" yaml:"mounts,omitempty"`
}
// Descriptor The static part of a plugin configuration.
type Descriptor struct {
// ModuleName (required)
@ -17,12 +22,18 @@ type Descriptor struct {
// Version (required)
Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"`
// Settings (optional)
Settings Settings `description:"Plugin's settings (works only for wasm plugins)." json:"settings,omitempty" toml:"settings,omitempty" yaml:"settings,omitempty" export:"true"`
}
// LocalDescriptor The static part of a local plugin configuration.
type LocalDescriptor struct {
// ModuleName (required)
ModuleName string `description:"plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty" export:"true"`
ModuleName string `description:"Plugin's module name." json:"moduleName,omitempty" toml:"moduleName,omitempty" yaml:"moduleName,omitempty" export:"true"`
// Settings (optional)
Settings Settings `description:"Plugin's settings (works only for wasm plugins)." json:"settings,omitempty" toml:"settings,omitempty" yaml:"settings,omitempty" export:"true"`
}
// Manifest The plugin manifest.

59
pkg/plugins/wasip.go Normal file
View file

@ -0,0 +1,59 @@
//go:build !windows
package plugins
import (
"context"
"fmt"
"os"
"github.com/stealthrocket/wasi-go/imports"
wazergo_wasip1 "github.com/stealthrocket/wasi-go/imports/wasi_snapshot_preview1"
"github.com/stealthrocket/wazergo"
"github.com/tetratelabs/wazero"
wazero_wasip1 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
type ContextApplier func(ctx context.Context) context.Context
// InstantiateHost instantiates the Host module according to the guest requirements (for now only SocketExtensions).
func InstantiateHost(ctx context.Context, runtime wazero.Runtime, mod wazero.CompiledModule, settings Settings) (ContextApplier, error) {
if extension := imports.DetectSocketsExtension(mod); extension != nil {
envs := []string{}
for _, env := range settings.Envs {
envs = append(envs, fmt.Sprintf("%s=%s", env, os.Getenv(env)))
}
builder := imports.NewBuilder().WithSocketsExtension("auto", mod)
if len(envs) > 0 {
builder.WithEnv(envs...)
}
if len(settings.Mounts) > 0 {
builder.WithDirs(settings.Mounts...)
}
ctx, sys, err := builder.Instantiate(ctx, runtime)
if err != nil {
return nil, err
}
inst, err := wazergo.Instantiate(ctx, runtime, wazergo_wasip1.NewHostModule(*extension), wazergo_wasip1.WithWASI(sys))
if err != nil {
return nil, fmt.Errorf("wazergo instantiation: %w", err)
}
return func(ctx context.Context) context.Context {
return wazergo.WithModuleInstance(ctx, inst)
}, nil
}
_, err := wazero_wasip1.Instantiate(ctx, runtime)
if err != nil {
return nil, fmt.Errorf("wazero instantiation: %w", err)
}
return func(ctx context.Context) context.Context {
return ctx
}, nil
}

View file

@ -0,0 +1,18 @@
//go:build windows
package plugins
import (
"context"
"github.com/tetratelabs/wazero"
)
type ContextApplier func(ctx context.Context) context.Context
// InstantiateHost instantiates the Host module.
func InstantiateHost(ctx context.Context, runtime wazero.Runtime, mod wazero.CompiledModule, settings Settings) (ContextApplier, error) {
return func(ctx context.Context) context.Context {
return ctx
}, nil
}