From f6dc8ce52c615e11f797bdd5d43f9e85f4652edd Mon Sep 17 00:00:00 2001 From: Matt Leon Date: Tue, 21 Jan 2025 14:32:43 -0500 Subject: [PATCH] Add entrypoints for Plugin, Http, Tcp contexts Fixes #5 Signed-off-by: Matt Leon --- README.md | 19 +++--- examples/helloworld/main.go | 16 +---- examples/helloworld/main_test.go | 14 ++-- examples/properties/main.go | 27 +------- examples/properties/main_test.go | 13 ++-- proxywasm/entrypoint.go | 54 ++++++++++++++- proxywasm/internal/entrypoints.go | 72 +++++++++++++++++++ proxywasm/internal/entrypoints_test.go | 95 ++++++++++++++++++++++++++ proxywasm/proxytest/option.go | 27 +++++++- proxywasm/proxytest/proxytest.go | 13 +++- proxywasm/types/context.go | 4 ++ 11 files changed, 287 insertions(+), 67 deletions(-) create mode 100644 proxywasm/internal/entrypoints.go create mode 100644 proxywasm/internal/entrypoints_test.go diff --git a/README.md b/README.md index 88c4d458..06dd6e81 100644 --- a/README.md +++ b/README.md @@ -59,20 +59,17 @@ import ( func main() {} func init() { - proxywasm.SetVMContext(&vmContext{}) + proxywasm.SetHttpContext(func(contextID uint32) types.HttpContext { + return &httpContext{} + }) } - -type vmContext struct { - types.DefaultVMContext -} -func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { - return &pluginContext{} +type httpContext struct { + types.DefaultHttpContext } - -type pluginContext struct { - types.DefaultPluginContext +func (*httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + proxywasm.LogInfo("Hello, world!") + return types.ActionContinue } -// pluginContext should implement OnPluginStart, NewHttpContext, NewTcpContext, etc ``` Compile the plugin as follows: diff --git a/examples/helloworld/main.go b/examples/helloworld/main.go index 547e029d..8324db22 100644 --- a/examples/helloworld/main.go +++ b/examples/helloworld/main.go @@ -26,19 +26,9 @@ const tickMilliseconds uint32 = 1000 func main() {} func init() { - proxywasm.SetVMContext(&vmContext{}) -} - -// vmContext implements types.VMContext. -type vmContext struct { - // Embed the default VM context here, - // so that we don't need to reimplement all the methods. - types.DefaultVMContext -} - -// NewPluginContext implements types.VMContext. -func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { - return &helloWorld{} + proxywasm.SetPluginContext(func(contextID uint32) types.PluginContext { + return &helloWorld{} + }) } // helloWorld implements types.PluginContext. diff --git a/examples/helloworld/main_test.go b/examples/helloworld/main_test.go index eca5b068..95c7cb37 100644 --- a/examples/helloworld/main_test.go +++ b/examples/helloworld/main_test.go @@ -14,8 +14,8 @@ import ( ) func TestHelloWorld_OnTick(t *testing.T) { - vmTest(t, func(t *testing.T, vm types.VMContext) { - opt := proxytest.NewEmulatorOption().WithVMContext(vm) + vmTest(t, func(t *testing.T, pc types.PluginContextFactory) { + opt := proxytest.NewEmulatorOption().WithPluginContext(pc) host, reset := proxytest.NewHostEmulator(opt) defer reset() @@ -33,8 +33,8 @@ func TestHelloWorld_OnTick(t *testing.T) { } func TestHelloWorld_OnPluginStart(t *testing.T) { - vmTest(t, func(t *testing.T, vm types.VMContext) { - opt := proxytest.NewEmulatorOption().WithVMContext(vm) + vmTest(t, func(t *testing.T, pc types.PluginContextFactory) { + opt := proxytest.NewEmulatorOption().WithPluginContext(pc) host, reset := proxytest.NewHostEmulator(opt) defer reset() @@ -51,11 +51,11 @@ func TestHelloWorld_OnPluginStart(t *testing.T) { // vmTest executes f twice, once with a types.VMContext that executes plugin code directly // in the host, and again by executing the plugin code within the compiled main.wasm binary. // Execution with main.wasm will be skipped if the file cannot be found. -func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { +func vmTest(t *testing.T, f func(*testing.T, types.PluginContextFactory)) { t.Helper() t.Run("go", func(t *testing.T) { - f(t, &vmContext{}) + f(t, func(uint32) types.PluginContext { return &helloWorld{} }) }) t.Run("wasm", func(t *testing.T) { @@ -66,6 +66,6 @@ func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { v, err := proxytest.NewWasmVMContext(wasm) require.NoError(t, err) defer v.Close() - f(t, v) + f(t, v.NewPluginContext) }) } diff --git a/examples/properties/main.go b/examples/properties/main.go index 06c9f3da..ef616a13 100644 --- a/examples/properties/main.go +++ b/examples/properties/main.go @@ -21,30 +21,9 @@ import ( func main() {} func init() { - proxywasm.SetVMContext(&vmContext{}) -} - -// vmContext implements types.VMContext. -type vmContext struct { - // Embed the default VM context here, - // so that we don't need to reimplement all the methods. - types.DefaultVMContext -} - -// NewPluginContext implements types.VMContext. -func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { - return &pluginContext{} -} - -type pluginContext struct { - // Embed the default plugin context here, - // so that we don't need to reimplement all the methods. - types.DefaultPluginContext -} - -// NewHttpContext implements types.PluginContext. -func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext { - return &properties{contextID: contextID} + proxywasm.SetHttpContext(func(contextID uint32) types.HttpContext { + return &properties{contextID: contextID} + }) } // properties implements types.HttpContext. diff --git a/examples/properties/main_test.go b/examples/properties/main_test.go index 9ec5ca3f..69550385 100644 --- a/examples/properties/main_test.go +++ b/examples/properties/main_test.go @@ -15,8 +15,8 @@ import ( ) func TestProperties_OnHttpRequestHeaders(t *testing.T) { - vmTest(t, func(t *testing.T, vm types.VMContext) { - opt := proxytest.NewEmulatorOption().WithVMContext(vm) + vmTest(t, func(t *testing.T, h types.HttpContextFactory) { + opt := proxytest.NewEmulatorOption().WithHttpContext(h) host, reset := proxytest.NewHostEmulator(opt) defer reset() @@ -90,11 +90,13 @@ func TestProperties_OnHttpRequestHeaders(t *testing.T) { // vmTest executes f twice, once with a types.VMContext that executes plugin code directly // in the host, and again by executing the plugin code within the compiled main.wasm binary. // Execution with main.wasm will be skipped if the file cannot be found. -func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { +func vmTest(t *testing.T, f func(*testing.T, types.HttpContextFactory)) { t.Helper() t.Run("go", func(t *testing.T) { - f(t, &vmContext{}) + f(t, func(contextID uint32) types.HttpContext { + return &properties{contextID: contextID} + }) }) t.Run("wasm", func(t *testing.T) { @@ -103,8 +105,9 @@ func vmTest(t *testing.T, f func(*testing.T, types.VMContext)) { t.Skip("wasm not found") } v, err := proxytest.NewWasmVMContext(wasm) + p := v.NewPluginContext(1) require.NoError(t, err) defer v.Close() - f(t, v) + f(t, p.NewHttpContext) }) } diff --git a/proxywasm/entrypoint.go b/proxywasm/entrypoint.go index cb458368..951b6ad1 100644 --- a/proxywasm/entrypoint.go +++ b/proxywasm/entrypoint.go @@ -19,9 +19,57 @@ import ( "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" ) -// SetVMContext is the entrypoint for setting up the entire Wasm VM. -// Please make sure to call this entrypoint during "main()" function; -// otherwise, the VM fails. +// SetVMContext is one possible entrypoint for setting up the entire Wasm VM. +// +// Subsequent calls to any entrypoint overwrite previous calls to any +// entrypoint. Be sure to call exactly one entrypoint during `init()`, +// otherwise the VM fails. func SetVMContext(ctx types.VMContext) { internal.SetVMContext(ctx) } + +// SetPluginContext is one possible entrypoint for setting up the Wasm VM. +// +// Subsequent calls to any entrypoint overwrite previous calls to any +// entrypoint. Be sure to call exactly one entrypoint during `init()`, +// otherwise the VM fails. +// +// Using SetPluginContext instead of SetVmContext is suitable iff the plugin +// does not make use of the VM configuration provided during `VmContext`'s +// `OnVmStart` call (plugin configuration data is still provided during +// `PluginContext`'s `OnPluginStart` call). +func SetPluginContext(newPluginContext func(contextID uint32) types.PluginContext) { + internal.SetPluginContext(newPluginContext) +} + +// SetHttpContext is one possible entrypoint for setting up the Wasm VM. It +// allows plugin authors to provide an Http context implementation without +// writing a VmContext or PluginContext. +// +// Subsequent calls to any entrypoint overwrite previous calls to any +// entrypoint. Be sure to call exactly one entrypoint during `init()`, +// otherwise the VM fails. +// +// SetHttpContext is suitable for stateless plugins that share no state between +// HTTP requests, do not process TCP streams, have no expensive shared setup +// requiring execution during `OnPluginStart`, and do not access the plugin +// configuration data. +func SetHttpContext(newHttpContext func(contextID uint32) types.HttpContext) { + internal.SetHttpContext(newHttpContext) +} + +// SetTcpContext is one possible entrypoint for setting up the Wasm VM. It +// allows plugin authors to provide a TCP context implementation without +// writing a VmContext or PluginContext. +// +// Subsequent calls to any entrypoint overwrite previous calls to any +// entrypoint. Be sure to call exactly one entrypoint during `init()`, +// otherwise the VM fails. +// +// SetTcpContext is suitable for stateless plugins that share no state between +// TCP streams, do not process HTTP requests, have no expensive shared setup +// requiring execution during `OnPluginStart`, and do not access the plugin +// configuration data. +func SetTcpContext(newTcpContext func(contextID uint32) types.TcpContext) { + internal.SetTcpContext(newTcpContext) +} diff --git a/proxywasm/internal/entrypoints.go b/proxywasm/internal/entrypoints.go new file mode 100644 index 00000000..25d439fd --- /dev/null +++ b/proxywasm/internal/entrypoints.go @@ -0,0 +1,72 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + +// elidedVmContext is registered when the plugin author uses a +// SetPluginContext, SetHttpContext, or SetTcpContext entrypoint. It indicates +// the author did not register a VmContext. +// +// elidedVmContext's primary responsibility is calling the author-provided (or +// elided) thunk to create a new PluginContext. +type elidedVmContext struct { + types.DefaultVMContext + newPluginContext types.PluginContextFactory +} + +func (ctx *elidedVmContext) NewPluginContext(contextID uint32) types.PluginContext { + return ctx.newPluginContext(contextID) +} + +// elidedPluginContext is registered when the plugin author uses the +// SetHttpContext or SetTcpContext entrypoints. It indicates the author did not +// register a VmContext or PluginContext. +// +// elidedVmContext's primary responsibility is calling the author-provided (or +// elided) thunk to create a new HttpContext or TcpContext. +type elidedPluginContext struct { + types.DefaultPluginContext + newHttpContext types.HttpContextFactory + newTcpContext types.TcpContextFactory +} + +func (ctx *elidedPluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return ctx.newHttpContext(contextID) +} + +func (ctx *elidedPluginContext) NewTcpContext(contextID uint32) types.TcpContext { + return ctx.newTcpContext(contextID) +} + +func SetPluginContext(newPluginContext types.PluginContextFactory) { + SetVMContext(&elidedVmContext{newPluginContext: newPluginContext}) +} + +func SetHttpContext(newHttpContext types.HttpContextFactory) { + SetVMContext(&elidedVmContext{ + newPluginContext: func(uint32) types.PluginContext { + return &elidedPluginContext{newHttpContext: newHttpContext} + }, + }) +} + +func SetTcpContext(newTcpContext types.TcpContextFactory) { + SetVMContext(&elidedVmContext{ + newPluginContext: func(uint32) types.PluginContext { + return &elidedPluginContext{newTcpContext: newTcpContext} + }, + }) +} diff --git a/proxywasm/internal/entrypoints_test.go b/proxywasm/internal/entrypoints_test.go new file mode 100644 index 00000000..81978567 --- /dev/null +++ b/proxywasm/internal/entrypoints_test.go @@ -0,0 +1,95 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "testing" + + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" + "github.com/stretchr/testify/require" +) + +type testPluginContext struct { + types.DefaultPluginContext + contextID uint32 +} + +func TestSetPluginContext_ReturnsPluginContext(t *testing.T) { + SetPluginContext(func(contextID uint32) types.PluginContext { + return &testPluginContext{contextID: contextID} + }) + + pluginContext := currentState.vmContext.NewPluginContext(4321) + + require.IsType(t, pluginContext, &testPluginContext{}) + require.Equal(t, pluginContext.(*testPluginContext).contextID, uint32(4321)) +} + +type testPluginContextB struct { + types.DefaultPluginContext +} + +func TestSetPluginContext_Reentrant(t *testing.T) { + SetPluginContext(func(uint32) types.PluginContext { + return &testPluginContext{} + }) + SetPluginContext(func(uint32) types.PluginContext { + return &testPluginContextB{} + }) + + require.IsType(t, currentState.vmContext.NewPluginContext(1), &testPluginContextB{}) +} + +type ( + testHttpContext struct { + types.DefaultHttpContext + contextID uint32 + } + testTcpContext struct{ types.DefaultTcpContext } +) + +func TestSetHttpContext(t *testing.T) { + SetHttpContext(func(contextID uint32) types.HttpContext { + return &testHttpContext{contextID: contextID} + }) + + pluginContext := currentState.vmContext.NewPluginContext(4321).NewHttpContext(1234) + + require.IsType(t, pluginContext, &testHttpContext{}) + require.Equal(t, pluginContext.(*testHttpContext).contextID, uint32(1234)) +} + +func TestSetTcpContext(t *testing.T) { + SetTcpContext(func(uint32) types.TcpContext { + return &testTcpContext{} + }) + + pluginContext := currentState.vmContext.NewPluginContext(4321).NewTcpContext(1234) + + require.IsType(t, pluginContext, &testTcpContext{}) +} + +func TestSetTcpContext_ClearsSetHttpContext(t *testing.T) { + SetHttpContext(func(contextID uint32) types.HttpContext { + return &testHttpContext{contextID: contextID} + }) + SetTcpContext(func(uint32) types.TcpContext { + return &testTcpContext{} + }) + + pluginContext := currentState.vmContext.NewPluginContext(4321).NewTcpContext(1234) + + require.IsType(t, pluginContext, &testTcpContext{}) +} diff --git a/proxywasm/proxytest/option.go b/proxywasm/proxytest/option.go index b01bce56..afb4a933 100644 --- a/proxywasm/proxytest/option.go +++ b/proxywasm/proxytest/option.go @@ -23,18 +23,39 @@ import ( type EmulatorOption struct { pluginConfiguration []byte vmConfiguration []byte - vmContext types.VMContext + context interface{} properties map[string][]byte } // NewEmulatorOption creates a new EmulatorOption. func NewEmulatorOption() *EmulatorOption { - return &EmulatorOption{vmContext: &types.DefaultVMContext{}} + return &EmulatorOption{context: &types.DefaultVMContext{}} } // WithVMContext sets the VMContext. func (o *EmulatorOption) WithVMContext(context types.VMContext) *EmulatorOption { - o.vmContext = context + o.context = context + return o +} + +// WithPluginContext sets up the emulator to use the passed func to construct +// PluginContexts with an anonymous VMContext. +func (o *EmulatorOption) WithPluginContext(context types.PluginContextFactory) *EmulatorOption { + o.context = context + return o +} + +// WithHttpContext sets up the emulator to use the passed func to construct +// HttpContexts an anonymous VMContext and PluginContext. +func (o *EmulatorOption) WithHttpContext(context types.HttpContextFactory) *EmulatorOption { + o.context = context + return o +} + +// WithTcpContext sets up the emulator to use the passed func to construct +// TcpContexts an anonymous VMContext and PluginContext. +func (o *EmulatorOption) WithTcpContext(context types.TcpContextFactory) *EmulatorOption { + o.context = context return o } diff --git a/proxywasm/proxytest/proxytest.go b/proxywasm/proxytest/proxytest.go index ed7e1f7c..620dbbcf 100644 --- a/proxywasm/proxytest/proxytest.go +++ b/proxywasm/proxytest/proxytest.go @@ -170,7 +170,18 @@ func NewHostEmulator(opt *EmulatorOption) (host HostEmulator, reset func()) { release := internal.RegisterMockWasmHost(emulator) // set up state - proxywasm.SetVMContext(opt.vmContext) + switch c := opt.context.(type) { + case types.VMContext: + proxywasm.SetVMContext(c) + case types.PluginContextFactory: + proxywasm.SetPluginContext(c) + case types.HttpContextFactory: + proxywasm.SetHttpContext(c) + case types.TcpContextFactory: + proxywasm.SetTcpContext(c) + default: + panic("Unknown context passed to NewHostEmulator.") + } // create plugin context: TODO: support multiple plugin contexts internal.ProxyOnContextCreate(PluginContextID, 0) diff --git a/proxywasm/types/context.go b/proxywasm/types/context.go index 577749ff..c142783e 100644 --- a/proxywasm/types/context.go +++ b/proxywasm/types/context.go @@ -221,3 +221,7 @@ var ( _ TcpContext = &DefaultTcpContext{} _ HttpContext = &DefaultHttpContext{} ) + +type PluginContextFactory func(contextID uint32) PluginContext +type HttpContextFactory func(contextID uint32) HttpContext +type TcpContextFactory func(contextID uint32) TcpContext