Add http-wasm plugin support to Traefik
This commit is contained in:
parent
b2bb96390a
commit
6858dbdd07
9 changed files with 338 additions and 119 deletions
2
go.mod
2
go.mod
|
@ -32,6 +32,7 @@ require (
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.4
|
github.com/hashicorp/go-retryablehttp v0.7.4
|
||||||
github.com/hashicorp/go-version v1.6.0
|
github.com/hashicorp/go-version v1.6.0
|
||||||
github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f
|
github.com/hashicorp/nomad/api v0.0.0-20220506174431-b5665129cd1f
|
||||||
|
github.com/http-wasm/http-wasm-host-go v0.5.2
|
||||||
github.com/influxdata/influxdb-client-go/v2 v2.7.0
|
github.com/influxdata/influxdb-client-go/v2 v2.7.0
|
||||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
|
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
|
||||||
github.com/instana/go-sensor v1.38.3
|
github.com/instana/go-sensor v1.38.3
|
||||||
|
@ -62,6 +63,7 @@ require (
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
|
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
|
||||||
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
|
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2
|
||||||
|
github.com/tetratelabs/wazero v1.5.0
|
||||||
github.com/traefik/grpc-web v0.16.0
|
github.com/traefik/grpc-web v0.16.0
|
||||||
github.com/traefik/paerser v0.2.0
|
github.com/traefik/paerser v0.2.0
|
||||||
github.com/traefik/yaegi v0.15.1
|
github.com/traefik/yaegi v0.15.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1085,6 +1085,8 @@ github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09Uny
|
||||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/http-wasm/http-wasm-host-go v0.5.2 h1:5d/QgaaJtTF+qd0goBaxJJ7tcHP9n+gQUldJ7TsTexA=
|
||||||
|
github.com/http-wasm/http-wasm-host-go v0.5.2/go.mod h1:zQB3w+df4hryDEqBorGyA1DwPJ86LfKIASNLFuj6CuI=
|
||||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
@ -1807,6 +1809,8 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 h1:mmz2
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 h1:g9SWTaTy/rEuhMErC2jWq9Qt5ci+jBYSvXnJsLq4adg=
|
||||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ=
|
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490/go.mod h1:l9q4vc1QiawUB1m3RU+87yLvrrxe54jc0w/kEl4DbSQ=
|
||||||
|
github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0=
|
||||||
|
github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A=
|
||||||
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
|
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
|
||||||
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
|
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
|
||||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||||
|
|
32
pkg/logs/wasm.go
Normal file
32
pkg/logs/wasm.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package logs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/http-wasm/http-wasm-host-go/api"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// compile-time check to ensure ConsoleLogger implements api.Logger.
|
||||||
|
var _ api.Logger = WasmLogger{}
|
||||||
|
|
||||||
|
// WasmLogger is a convenience which writes anything above LogLevelInfo to os.Stdout.
|
||||||
|
type WasmLogger struct {
|
||||||
|
logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWasmLogger(logger *zerolog.Logger) *WasmLogger {
|
||||||
|
return &WasmLogger{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled implements the same method as documented on api.Logger.
|
||||||
|
func (w WasmLogger) IsEnabled(level api.LogLevel) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements the same method as documented on api.Logger.
|
||||||
|
func (w WasmLogger) Log(_ context.Context, level api.LogLevel, message string) {
|
||||||
|
w.logger.WithLevel(zerolog.Level(level + 1)).Msg(message)
|
||||||
|
}
|
|
@ -4,28 +4,34 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v3/pkg/logs"
|
|
||||||
"github.com/traefik/yaegi/interp"
|
|
||||||
"github.com/traefik/yaegi/stdlib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Constructor creates a plugin handler.
|
// Constructor creates a plugin handler.
|
||||||
type Constructor func(context.Context, http.Handler) (http.Handler, error)
|
type Constructor func(context.Context, http.Handler) (http.Handler, error)
|
||||||
|
|
||||||
|
type pluginMiddleware interface {
|
||||||
|
NewHandler(ctx context.Context, next http.Handler) (http.Handler, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type middlewareBuilder interface {
|
||||||
|
newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Builder is a plugin builder.
|
// Builder is a plugin builder.
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
middlewareBuilders map[string]*middlewareBuilder
|
|
||||||
providerBuilders map[string]providerBuilder
|
providerBuilders map[string]providerBuilder
|
||||||
|
middlewareBuilders map[string]middlewareBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder creates a new Builder.
|
// NewBuilder creates a new Builder.
|
||||||
func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
|
func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
pb := &Builder{
|
pb := &Builder{
|
||||||
middlewareBuilders: map[string]*middlewareBuilder{},
|
middlewareBuilders: map[string]middlewareBuilder{},
|
||||||
providerBuilders: map[string]providerBuilder{},
|
providerBuilders: map[string]providerBuilder{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,44 +42,30 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[
|
||||||
return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
|
return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
|
logger := log.With().
|
||||||
|
Str("plugin", "plugin-"+pName).
|
||||||
i := interp.New(interp.Options{
|
Str("module", desc.ModuleName).
|
||||||
GoPath: client.GoPath(),
|
Str("runtime", manifest.Runtime).
|
||||||
Env: os.Environ(),
|
Logger()
|
||||||
Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
|
logCtx := logger.WithContext(ctx)
|
||||||
Stderr: logs.NoLevel(logger, zerolog.ErrorLevel),
|
|
||||||
})
|
|
||||||
|
|
||||||
err = i.Use(stdlib.Symbols)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = i.Use(ppSymbols())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch manifest.Type {
|
switch manifest.Type {
|
||||||
case "middleware":
|
case typeMiddleware:
|
||||||
middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
|
middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pb.middlewareBuilders[pName] = middleware
|
pb.middlewareBuilders[pName] = middleware
|
||||||
case "provider":
|
|
||||||
pb.providerBuilders[pName] = providerBuilder{
|
case typeProvider:
|
||||||
interpreter: i,
|
pBuilder, err := newProviderBuilder(logCtx, manifest, client.GoPath())
|
||||||
Import: manifest.Import,
|
if err != nil {
|
||||||
BasePkg: manifest.BasePkg,
|
return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pb.providerBuilders[pName] = pBuilder
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
|
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
|
||||||
}
|
}
|
||||||
|
@ -85,48 +77,107 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[
|
||||||
return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
|
return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
|
logger := log.With().
|
||||||
|
Str("plugin", "plugin-"+pName).
|
||||||
i := interp.New(interp.Options{
|
Str("module", desc.ModuleName).
|
||||||
GoPath: localGoPath,
|
Str("runtime", manifest.Runtime).
|
||||||
Env: os.Environ(),
|
Logger()
|
||||||
Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
|
logCtx := logger.WithContext(ctx)
|
||||||
Stderr: logs.NoLevel(logger, zerolog.ErrorLevel),
|
|
||||||
})
|
|
||||||
|
|
||||||
err = i.Use(stdlib.Symbols)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = i.Use(ppSymbols())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch manifest.Type {
|
switch manifest.Type {
|
||||||
case "middleware":
|
case typeMiddleware:
|
||||||
middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
|
middleware, err := newMiddlewareBuilder(logCtx, localGoPath, manifest, desc.ModuleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pb.middlewareBuilders[pName] = middleware
|
pb.middlewareBuilders[pName] = middleware
|
||||||
case "provider":
|
|
||||||
pb.providerBuilders[pName] = providerBuilder{
|
case typeProvider:
|
||||||
interpreter: i,
|
builder, err := newProviderBuilder(logCtx, manifest, localGoPath)
|
||||||
Import: manifest.Import,
|
if err != nil {
|
||||||
BasePkg: manifest.BasePkg,
|
return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pb.providerBuilders[pName] = builder
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
|
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pb, nil
|
return pb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build builds a middleware plugin.
|
||||||
|
func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) {
|
||||||
|
if b.middlewareBuilders == nil {
|
||||||
|
return nil, fmt.Errorf("no plugin definitions in the static configuration: %s", pName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugin (pName) can be located in yaegi or wasm middleware builders.
|
||||||
|
if descriptor, ok := b.middlewareBuilders[pName]; ok {
|
||||||
|
m, err := descriptor.newMiddleware(config, middlewareName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.NewHandler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown plugin type: %s", pName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMiddlewareBuilder(ctx context.Context, goPath string, manifest *Manifest, moduleName string) (middlewareBuilder, error) {
|
||||||
|
switch manifest.Runtime {
|
||||||
|
case runtimeWasm:
|
||||||
|
wasmPath, err := getWasmPath(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wasm path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWasmMiddlewareBuilder(goPath, moduleName, wasmPath), nil
|
||||||
|
|
||||||
|
case runtimeYaegi, "":
|
||||||
|
i, err := newInterpreter(ctx, goPath, manifest.Import)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to craete Yaegi intepreter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newYaegiMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown plugin runtime: %s", manifest.Runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProviderBuilder(ctx context.Context, manifest *Manifest, goPath string) (providerBuilder, error) {
|
||||||
|
switch manifest.Runtime {
|
||||||
|
case runtimeYaegi, "":
|
||||||
|
i, err := newInterpreter(ctx, goPath, manifest.Import)
|
||||||
|
if err != nil {
|
||||||
|
return providerBuilder{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerBuilder{
|
||||||
|
interpreter: i,
|
||||||
|
Import: manifest.Import,
|
||||||
|
BasePkg: manifest.BasePkg,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return providerBuilder{}, fmt.Errorf("unknown plugin runtime: %s", manifest.Runtime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWasmPath(manifest *Manifest) (string, error) {
|
||||||
|
wasmPath := manifest.WasmPath
|
||||||
|
if wasmPath == "" {
|
||||||
|
wasmPath = "plugin.wasm"
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filepath.IsLocal(wasmPath) {
|
||||||
|
return "", fmt.Errorf("wasmPath must be a local path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return wasmPath, nil
|
||||||
|
}
|
||||||
|
|
|
@ -279,7 +279,15 @@ func unzipFile(f *zipa.File, dest string) error {
|
||||||
defer func() { _ = rc.Close() }()
|
defer func() { _ = rc.Close() }()
|
||||||
|
|
||||||
pathParts := strings.SplitN(f.Name, "/", 2)
|
pathParts := strings.SplitN(f.Name, "/", 2)
|
||||||
p := filepath.Join(dest, pathParts[1])
|
|
||||||
|
var pp string
|
||||||
|
if len(pathParts) < 2 {
|
||||||
|
pp = pathParts[0]
|
||||||
|
} else {
|
||||||
|
pp = pathParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
p := filepath.Join(dest, pp)
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
err = os.MkdirAll(p, f.Mode())
|
err = os.MkdirAll(p, f.Mode())
|
||||||
|
|
81
pkg/plugins/middlewarewasm.go
Normal file
81
pkg/plugins/middlewarewasm.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/http-wasm/http-wasm-host-go/handler"
|
||||||
|
wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp"
|
||||||
|
"github.com/tetratelabs/wazero"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/logs"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/middlewares"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wasmMiddlewareBuilder struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWasmMiddlewareBuilder(goPath string, moduleName, wasmPath string) *wasmMiddlewareBuilder {
|
||||||
|
return &wasmMiddlewareBuilder{path: filepath.Join(goPath, "src", moduleName, wasmPath)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
|
||||||
|
return &WasmMiddleware{
|
||||||
|
middlewareName: middlewareName,
|
||||||
|
config: reflect.ValueOf(config),
|
||||||
|
builder: b,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
|
||||||
|
code, err := os.ReadFile(b.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loading Wasm binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := middlewares.GetLogger(ctx, middlewareName, "wasm")
|
||||||
|
|
||||||
|
opts := []handler.Option{
|
||||||
|
handler.ModuleConfig(wazero.NewModuleConfig().WithSysWalltime()),
|
||||||
|
handler.Logger(logs.NewWasmLogger(logger)),
|
||||||
|
}
|
||||||
|
|
||||||
|
i := cfg.Interface()
|
||||||
|
if i != nil {
|
||||||
|
config, ok := i.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, handler.GuestConfig(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
mw, err := wasm.NewMiddleware(context.Background(), code, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mw.NewHandler(ctx, next), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WasmMiddleware is an HTTP handler plugin wrapper.
|
||||||
|
type WasmMiddleware struct {
|
||||||
|
middlewareName string
|
||||||
|
config reflect.Value
|
||||||
|
builder wasmMiddlewareBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new HTTP handler.
|
||||||
|
func (m WasmMiddleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
|
||||||
|
return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
|
||||||
|
}
|
|
@ -4,39 +4,25 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/traefik/traefik/v3/pkg/logs"
|
||||||
"github.com/traefik/yaegi/interp"
|
"github.com/traefik/yaegi/interp"
|
||||||
|
"github.com/traefik/yaegi/stdlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build builds a middleware plugin.
|
type yaegiMiddlewareBuilder struct {
|
||||||
func (b Builder) Build(pName string, config map[string]interface{}, middlewareName string) (Constructor, error) {
|
|
||||||
if b.middlewareBuilders == nil {
|
|
||||||
return nil, fmt.Errorf("no plugin definition in the static configuration: %s", pName)
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor, ok := b.middlewareBuilders[pName]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unknown plugin type: %s", pName)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := newMiddleware(descriptor, config, middlewareName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.NewHandler, err
|
|
||||||
}
|
|
||||||
|
|
||||||
type middlewareBuilder struct {
|
|
||||||
fnNew reflect.Value
|
fnNew reflect.Value
|
||||||
fnCreateConfig reflect.Value
|
fnCreateConfig reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*middlewareBuilder, error) {
|
func newYaegiMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*yaegiMiddlewareBuilder, error) {
|
||||||
if basePkg == "" {
|
if basePkg == "" {
|
||||||
basePkg = strings.ReplaceAll(path.Base(imp), "-", "_")
|
basePkg = strings.ReplaceAll(path.Base(imp), "-", "_")
|
||||||
}
|
}
|
||||||
|
@ -51,21 +37,35 @@ func newMiddlewareBuilder(i *interp.Interpreter, basePkg, imp string) (*middlewa
|
||||||
return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
|
return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &middlewareBuilder{
|
return &yaegiMiddlewareBuilder{
|
||||||
fnNew: fnNew,
|
fnNew: fnNew,
|
||||||
fnCreateConfig: fnCreateConfig,
|
fnCreateConfig: fnCreateConfig,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p middlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
|
func (b yaegiMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
|
||||||
|
vConfig, err := b.createConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &YaegiMiddleware{
|
||||||
|
middlewareName: middlewareName,
|
||||||
|
config: vConfig,
|
||||||
|
builder: b,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b yaegiMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
|
||||||
args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), cfg, reflect.ValueOf(middlewareName)}
|
args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(next), cfg, reflect.ValueOf(middlewareName)}
|
||||||
results := p.fnNew.Call(args)
|
results := b.fnNew.Call(args)
|
||||||
|
|
||||||
if len(results) > 1 && results[1].Interface() != nil {
|
if len(results) > 1 && results[1].Interface() != nil {
|
||||||
err, ok := results[1].Interface().(error)
|
err, ok := results[1].Interface().(error)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid error type: %T", results[0].Interface())
|
return nil, fmt.Errorf("invalid error type: %T", results[0].Interface())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,8 +77,8 @@ func (p middlewareBuilder) newHandler(ctx context.Context, next http.Handler, cf
|
||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p middlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
|
func (b yaegiMiddlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
|
||||||
results := p.fnCreateConfig.Call(nil)
|
results := b.fnCreateConfig.Call(nil)
|
||||||
if len(results) != 1 {
|
if len(results) != 1 {
|
||||||
return reflect.Value{}, fmt.Errorf("invalid number of return for the CreateConfig function: %d", len(results))
|
return reflect.Value{}, fmt.Errorf("invalid number of return for the CreateConfig function: %d", len(results))
|
||||||
}
|
}
|
||||||
|
@ -107,27 +107,40 @@ func (p middlewareBuilder) createConfig(config map[string]interface{}) (reflect.
|
||||||
return vConfig, nil
|
return vConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middleware is an HTTP handler plugin wrapper.
|
// YaegiMiddleware is an HTTP handler plugin wrapper.
|
||||||
type Middleware struct {
|
type YaegiMiddleware struct {
|
||||||
middlewareName string
|
middlewareName string
|
||||||
config reflect.Value
|
config reflect.Value
|
||||||
builder *middlewareBuilder
|
builder yaegiMiddlewareBuilder
|
||||||
}
|
|
||||||
|
|
||||||
func newMiddleware(builder *middlewareBuilder, config map[string]interface{}, middlewareName string) (*Middleware, error) {
|
|
||||||
vConfig, err := builder.createConfig(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Middleware{
|
|
||||||
middlewareName: middlewareName,
|
|
||||||
config: vConfig,
|
|
||||||
builder: builder,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new HTTP handler.
|
// NewHandler creates a new HTTP handler.
|
||||||
func (m *Middleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
|
func (m *YaegiMiddleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
|
||||||
return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
|
return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newInterpreter(ctx context.Context, goPath string, manifestImport string) (*interp.Interpreter, error) {
|
||||||
|
i := interp.New(interp.Options{
|
||||||
|
GoPath: goPath,
|
||||||
|
Env: os.Environ(),
|
||||||
|
Stdout: logs.NoLevel(*log.Ctx(ctx), zerolog.DebugLevel),
|
||||||
|
Stderr: logs.NoLevel(*log.Ctx(ctx), zerolog.ErrorLevel),
|
||||||
|
})
|
||||||
|
|
||||||
|
err := i.Use(stdlib.Symbols)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load symbols: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.Use(ppSymbols())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider symbols: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = i.Eval(fmt.Sprintf(`import "%s"`, manifestImport))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to import plugin code %q: %w", manifestImport, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
|
@ -138,12 +138,21 @@ func checkLocalPluginManifest(descriptor LocalDescriptor) error {
|
||||||
var errs *multierror.Error
|
var errs *multierror.Error
|
||||||
|
|
||||||
switch m.Type {
|
switch m.Type {
|
||||||
case "middleware", "provider":
|
case typeMiddleware:
|
||||||
// noop
|
if m.Runtime != runtimeYaegi && m.Runtime != runtimeWasm && m.Runtime != "" {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("%s: unsupported runtime '%q'", descriptor.ModuleName, m.Runtime))
|
||||||
|
}
|
||||||
|
|
||||||
|
case typeProvider:
|
||||||
|
if m.Runtime != runtimeYaegi && m.Runtime != "" {
|
||||||
|
errs = multierror.Append(errs, fmt.Errorf("%s: unsupported runtime '%q'", descriptor.ModuleName, m.Runtime))
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
errs = multierror.Append(errs, fmt.Errorf("%s: unsupported type %q", descriptor.ModuleName, m.Type))
|
errs = multierror.Append(errs, fmt.Errorf("%s: unsupported type %q", descriptor.ModuleName, m.Type))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.IsYaegiPlugin() {
|
||||||
if m.Import == "" {
|
if m.Import == "" {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName))
|
errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName))
|
||||||
}
|
}
|
||||||
|
@ -151,6 +160,7 @@ func checkLocalPluginManifest(descriptor LocalDescriptor) error {
|
||||||
if !strings.HasPrefix(m.Import, descriptor.ModuleName) {
|
if !strings.HasPrefix(m.Import, descriptor.ModuleName) {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("the import %q must be related to the module name %q", m.Import, descriptor.ModuleName))
|
errs = multierror.Append(errs, fmt.Errorf("the import %q must be related to the module name %q", m.Import, descriptor.ModuleName))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if m.DisplayName == "" {
|
if m.DisplayName == "" {
|
||||||
errs = multierror.Append(errs, fmt.Errorf("%s: missing DisplayName", descriptor.ModuleName))
|
errs = multierror.Append(errs, fmt.Errorf("%s: missing DisplayName", descriptor.ModuleName))
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
|
const (
|
||||||
|
runtimeYaegi = "yaegi"
|
||||||
|
runtimeWasm = "wasm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeMiddleware = "middleware"
|
||||||
|
typeProvider = "provider"
|
||||||
|
)
|
||||||
|
|
||||||
// Descriptor The static part of a plugin configuration.
|
// Descriptor The static part of a plugin configuration.
|
||||||
type Descriptor struct {
|
type Descriptor struct {
|
||||||
// ModuleName (required)
|
// ModuleName (required)
|
||||||
|
@ -19,9 +29,17 @@ type LocalDescriptor struct {
|
||||||
type Manifest struct {
|
type Manifest struct {
|
||||||
DisplayName string `yaml:"displayName"`
|
DisplayName string `yaml:"displayName"`
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
|
Runtime string `yaml:"runtime"`
|
||||||
|
WasmPath string `yaml:"wasmPath"`
|
||||||
Import string `yaml:"import"`
|
Import string `yaml:"import"`
|
||||||
BasePkg string `yaml:"basePkg"`
|
BasePkg string `yaml:"basePkg"`
|
||||||
Compatibility string `yaml:"compatibility"`
|
Compatibility string `yaml:"compatibility"`
|
||||||
Summary string `yaml:"summary"`
|
Summary string `yaml:"summary"`
|
||||||
TestData map[string]interface{} `yaml:"testData"`
|
TestData map[string]interface{} `yaml:"testData"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsYaegiPlugin returns true if the plugin is a Yaegi plugin.
|
||||||
|
func (m *Manifest) IsYaegiPlugin() bool {
|
||||||
|
// defaults always Yaegi to have backwards compatibility to plugins without runtime
|
||||||
|
return m.Runtime == runtimeYaegi || m.Runtime == ""
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue