diff --git a/go.mod b/go.mod index 7209fc0ab..0089585ce 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 543eb0b07..1f07133c4 100644 --- a/go.sum +++ b/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= diff --git a/pkg/logs/wasm.go b/pkg/logs/wasm.go new file mode 100644 index 000000000..11d3e634f --- /dev/null +++ b/pkg/logs/wasm.go @@ -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) +} diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index c2012427b..42f1a5f05 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -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 +} diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go index 44a85672c..52369585f 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/client.go @@ -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()) diff --git a/pkg/plugins/middlewarewasm.go b/pkg/plugins/middlewarewasm.go new file mode 100644 index 000000000..b307850d9 --- /dev/null +++ b/pkg/plugins/middlewarewasm.go @@ -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) +} diff --git a/pkg/plugins/middlewares.go b/pkg/plugins/middlewareyaegi.go similarity index 54% rename from pkg/plugins/middlewares.go rename to pkg/plugins/middlewareyaegi.go index e46dbae34..452f8713b 100644 --- a/pkg/plugins/middlewares.go +++ b/pkg/plugins/middlewareyaegi.go @@ -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 +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 43beee8f0..367b6c46c 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -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 == "" { diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index 78ea13a34..35b357faf 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -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 == "" +}