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-version v1.6.0
|
||||
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/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
|
||||
github.com/instana/go-sensor v1.38.3
|
||||
|
@ -62,6 +63,7 @@ require (
|
|||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stvp/go-udp-testing v0.0.0-20191102171040-06b61409b154
|
||||
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/paerser v0.2.0
|
||||
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/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
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.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
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/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/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/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
|
||||
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"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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/stdlib"
|
||||
)
|
||||
|
||||
// Constructor creates a plugin handler.
|
||||
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.
|
||||
type Builder struct {
|
||||
middlewareBuilders map[string]*middlewareBuilder
|
||||
providerBuilders map[string]providerBuilder
|
||||
middlewareBuilders map[string]middlewareBuilder
|
||||
}
|
||||
|
||||
// NewBuilder creates a new Builder.
|
||||
func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
pb := &Builder{
|
||||
middlewareBuilders: map[string]*middlewareBuilder{},
|
||||
middlewareBuilders: map[string]middlewareBuilder{},
|
||||
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)
|
||||
}
|
||||
|
||||
logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
|
||||
|
||||
i := interp.New(interp.Options{
|
||||
GoPath: client.GoPath(),
|
||||
Env: os.Environ(),
|
||||
Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
|
||||
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)
|
||||
}
|
||||
logger := log.With().
|
||||
Str("plugin", "plugin-"+pName).
|
||||
Str("module", desc.ModuleName).
|
||||
Str("runtime", manifest.Runtime).
|
||||
Logger()
|
||||
logCtx := logger.WithContext(ctx)
|
||||
|
||||
switch manifest.Type {
|
||||
case "middleware":
|
||||
middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
|
||||
case typeMiddleware:
|
||||
middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pb.middlewareBuilders[pName] = middleware
|
||||
case "provider":
|
||||
pb.providerBuilders[pName] = providerBuilder{
|
||||
interpreter: i,
|
||||
Import: manifest.Import,
|
||||
BasePkg: manifest.BasePkg,
|
||||
|
||||
case typeProvider:
|
||||
pBuilder, err := newProviderBuilder(logCtx, manifest, client.GoPath())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
|
||||
}
|
||||
|
||||
pb.providerBuilders[pName] = pBuilder
|
||||
|
||||
default:
|
||||
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)
|
||||
}
|
||||
|
||||
logger := log.With().Str("plugin", "plugin-"+pName).Str("module", desc.ModuleName).Logger()
|
||||
|
||||
i := interp.New(interp.Options{
|
||||
GoPath: localGoPath,
|
||||
Env: os.Environ(),
|
||||
Stdout: logs.NoLevel(logger, zerolog.DebugLevel),
|
||||
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)
|
||||
}
|
||||
logger := log.With().
|
||||
Str("plugin", "plugin-"+pName).
|
||||
Str("module", desc.ModuleName).
|
||||
Str("runtime", manifest.Runtime).
|
||||
Logger()
|
||||
logCtx := logger.WithContext(ctx)
|
||||
|
||||
switch manifest.Type {
|
||||
case "middleware":
|
||||
middleware, err := newMiddlewareBuilder(i, manifest.BasePkg, manifest.Import)
|
||||
case typeMiddleware:
|
||||
middleware, err := newMiddlewareBuilder(logCtx, localGoPath, manifest, desc.ModuleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pb.middlewareBuilders[pName] = middleware
|
||||
case "provider":
|
||||
pb.providerBuilders[pName] = providerBuilder{
|
||||
interpreter: i,
|
||||
Import: manifest.Import,
|
||||
BasePkg: manifest.BasePkg,
|
||||
|
||||
case typeProvider:
|
||||
builder, err := newProviderBuilder(logCtx, manifest, localGoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", desc.ModuleName, err)
|
||||
}
|
||||
|
||||
pb.providerBuilders[pName] = builder
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
|
||||
}
|
||||
}
|
||||
|
||||
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() }()
|
||||
|
||||
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() {
|
||||
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"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"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/stdlib"
|
||||
)
|
||||
|
||||
// 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 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 {
|
||||
type yaegiMiddlewareBuilder struct {
|
||||
fnNew 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 == "" {
|
||||
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 &middlewareBuilder{
|
||||
return &yaegiMiddlewareBuilder{
|
||||
fnNew: fnNew,
|
||||
fnCreateConfig: fnCreateConfig,
|
||||
}, 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)}
|
||||
results := p.fnNew.Call(args)
|
||||
results := b.fnNew.Call(args)
|
||||
|
||||
if len(results) > 1 && results[1].Interface() != nil {
|
||||
err, ok := results[1].Interface().(error)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid error type: %T", results[0].Interface())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -77,8 +77,8 @@ func (p middlewareBuilder) newHandler(ctx context.Context, next http.Handler, cf
|
|||
return handler, nil
|
||||
}
|
||||
|
||||
func (p middlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
|
||||
results := p.fnCreateConfig.Call(nil)
|
||||
func (b yaegiMiddlewareBuilder) createConfig(config map[string]interface{}) (reflect.Value, error) {
|
||||
results := b.fnCreateConfig.Call(nil)
|
||||
if len(results) != 1 {
|
||||
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
|
||||
}
|
||||
|
||||
// Middleware is an HTTP handler plugin wrapper.
|
||||
type Middleware struct {
|
||||
// YaegiMiddleware is an HTTP handler plugin wrapper.
|
||||
type YaegiMiddleware struct {
|
||||
middlewareName string
|
||||
config reflect.Value
|
||||
builder *middlewareBuilder
|
||||
}
|
||||
|
||||
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
|
||||
builder yaegiMiddlewareBuilder
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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,18 +138,28 @@ func checkLocalPluginManifest(descriptor LocalDescriptor) error {
|
|||
var errs *multierror.Error
|
||||
|
||||
switch m.Type {
|
||||
case "middleware", "provider":
|
||||
// noop
|
||||
case typeMiddleware:
|
||||
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:
|
||||
errs = multierror.Append(errs, fmt.Errorf("%s: unsupported type %q", descriptor.ModuleName, m.Type))
|
||||
}
|
||||
|
||||
if m.Import == "" {
|
||||
errs = multierror.Append(errs, fmt.Errorf("%s: missing import", descriptor.ModuleName))
|
||||
}
|
||||
if m.IsYaegiPlugin() {
|
||||
if m.Import == "" {
|
||||
errs = multierror.Append(errs, fmt.Errorf("%s: missing 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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
if m.DisplayName == "" {
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
package plugins
|
||||
|
||||
const (
|
||||
runtimeYaegi = "yaegi"
|
||||
runtimeWasm = "wasm"
|
||||
)
|
||||
|
||||
const (
|
||||
typeMiddleware = "middleware"
|
||||
typeProvider = "provider"
|
||||
)
|
||||
|
||||
// Descriptor The static part of a plugin configuration.
|
||||
type Descriptor struct {
|
||||
// ModuleName (required)
|
||||
|
@ -19,9 +29,17 @@ type LocalDescriptor struct {
|
|||
type Manifest struct {
|
||||
DisplayName string `yaml:"displayName"`
|
||||
Type string `yaml:"type"`
|
||||
Runtime string `yaml:"runtime"`
|
||||
WasmPath string `yaml:"wasmPath"`
|
||||
Import string `yaml:"import"`
|
||||
BasePkg string `yaml:"basePkg"`
|
||||
Compatibility string `yaml:"compatibility"`
|
||||
Summary string `yaml:"summary"`
|
||||
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