From 6321dc16227c577c8e339df3b377f1c6e8c8a1ea Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 28 Dec 2025 12:35:49 -0500 Subject: [PATCH] feat(plugins): add subsonicRouter to Manager and refactor host service registration Signed-off-by: Deluan --- plugins/host_artwork_test.go | 6 +- plugins/host_cache_test.go | 6 +- plugins/host_scheduler_test.go | 6 +- plugins/host_websocket_test.go | 5 +- plugins/manager.go | 129 ++++++++++++++++++++++----------- plugins/plugins_suite_test.go | 6 +- plugins/watcher_test.go | 6 +- 7 files changed, 111 insertions(+), 53 deletions(-) diff --git a/plugins/host_artwork_test.go b/plugins/host_artwork_test.go index b1b55bf15..64fd7ff8b 100644 --- a/plugins/host_artwork_test.go +++ b/plugins/host_artwork_test.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "net/http" "os" "path/filepath" "strings" @@ -70,8 +71,9 @@ var _ = Describe("ArtworkService", Ordered, func() { // Create and start manager manager = &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/host_cache_test.go b/plugins/host_cache_test.go index 863a4ad46..4a5580e55 100644 --- a/plugins/host_cache_test.go +++ b/plugins/host_cache_test.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "net/http" "os" "path/filepath" "time" @@ -359,8 +360,9 @@ var _ = Describe("CacheService Integration", Ordered, func() { // Create and start manager manager = &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/host_scheduler_test.go b/plugins/host_scheduler_test.go index e56ef31ef..d32980ae1 100644 --- a/plugins/host_scheduler_test.go +++ b/plugins/host_scheduler_test.go @@ -6,6 +6,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "net/http" "os" "path/filepath" "sync" @@ -75,8 +76,9 @@ var _ = Describe("SchedulerService", Ordered, func() { // Create and start manager manager = &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/host_websocket_test.go b/plugins/host_websocket_test.go index 7a3248efc..934f2da62 100644 --- a/plugins/host_websocket_test.go +++ b/plugins/host_websocket_test.go @@ -68,8 +68,9 @@ var _ = Describe("WebSocketService", Ordered, func() { // Create and start manager manager = &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/manager.go b/plugins/manager.go index f0f0528b1..f04ea4fe4 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -136,6 +136,10 @@ func (m *Manager) Start(ctx context.Context) error { return nil } + if m.subsonicRouter == nil { + log.Fatal(ctx, "Plugin manager requires DataStore to be configured") + } + // Set extism log level based on plugin-specific config or global log level pluginLogLevel := conf.Server.Plugins.LogLevel if pluginLogLevel == "" { @@ -411,13 +415,79 @@ type compiledPluginInfo struct { compiled *extism.CompiledPlugin } +// serviceContext provides dependencies needed by host service factories. +type serviceContext struct { + pluginName string + manager *Manager + permissions *Permissions +} + +// hostServiceEntry defines a host service for table-driven registration. +type hostServiceEntry struct { + name string + hasPermission func(*Permissions) bool + registerStubs func() []extism.HostFunction + create func(*serviceContext) ([]extism.HostFunction, io.Closer) +} + +// hostServices defines all available host services. +// Adding a new host service only requires adding an entry here. +var hostServices = []hostServiceEntry{ + { + name: "SubsonicAPI", + hasPermission: func(p *Permissions) bool { return p != nil && p.Subsonicapi != nil }, + registerStubs: func() []extism.HostFunction { return host.RegisterSubsonicAPIHostFunctions(nil) }, + create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) { + perm := ctx.permissions.Subsonicapi + service := newSubsonicAPIService(ctx.pluginName, ctx.manager.subsonicRouter, ctx.manager.ds, perm) + return host.RegisterSubsonicAPIHostFunctions(service), nil + }, + }, + { + name: "Scheduler", + hasPermission: func(p *Permissions) bool { return p != nil && p.Scheduler != nil }, + registerStubs: func() []extism.HostFunction { return host.RegisterSchedulerHostFunctions(nil) }, + create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) { + service := newSchedulerService(ctx.pluginName, ctx.manager, scheduler.GetInstance()) + return host.RegisterSchedulerHostFunctions(service), service + }, + }, + { + name: "WebSocket", + hasPermission: func(p *Permissions) bool { return p != nil && p.Websocket != nil }, + registerStubs: func() []extism.HostFunction { return host.RegisterWebSocketHostFunctions(nil) }, + create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) { + perm := ctx.permissions.Websocket + service := newWebSocketService(ctx.pluginName, ctx.manager, perm.AllowedHosts) + return host.RegisterWebSocketHostFunctions(service), service + }, + }, + { + name: "Artwork", + hasPermission: func(p *Permissions) bool { return p != nil && p.Artwork != nil }, + registerStubs: func() []extism.HostFunction { return host.RegisterArtworkHostFunctions(nil) }, + create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) { + service := newArtworkService() + return host.RegisterArtworkHostFunctions(service), nil + }, + }, + { + name: "Cache", + hasPermission: func(p *Permissions) bool { return p != nil && p.Cache != nil }, + registerStubs: func() []extism.HostFunction { return host.RegisterCacheHostFunctions(nil) }, + create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) { + service := newCacheService(ctx.pluginName) + return host.RegisterCacheHostFunctions(service), service + }, + }, +} + // stubHostFunctions returns the list of stub host functions needed for initial plugin compilation. func stubHostFunctions() []extism.HostFunction { - stubs := host.RegisterSubsonicAPIHostFunctions(nil) - stubs = append(stubs, host.RegisterSchedulerHostFunctions(nil)...) - stubs = append(stubs, host.RegisterWebSocketHostFunctions(nil)...) - stubs = append(stubs, host.RegisterArtworkHostFunctions(nil)...) - stubs = append(stubs, host.RegisterCacheHostFunctions(nil)...) + var stubs []extism.HostFunction + for _, entry := range hostServices { + stubs = append(stubs, entry.registerStubs()...) + } return stubs } @@ -748,45 +818,22 @@ func (m *Manager) loadPluginWithConfig(name, wasmPath, configJSON string) error pluginManifest.AllowedHosts = hosts } - // Register SubsonicAPI host functions if permission is granted - if info.manifest.Permissions != nil && info.manifest.Permissions.Subsonicapi != nil { - perm := info.manifest.Permissions.Subsonicapi - if m.subsonicRouter != nil && m.ds != nil { - service := newSubsonicAPIService(name, m.subsonicRouter, m.ds, perm) - hostFunctions = append(hostFunctions, host.RegisterSubsonicAPIHostFunctions(service)...) - } else { - log.Warn(m.ctx, "Plugin requires SubsonicAPI but router/datastore not available", "plugin", name) + // Register host functions based on permissions using table-driven approach + svcCtx := &serviceContext{ + pluginName: name, + manager: m, + permissions: info.manifest.Permissions, + } + for _, entry := range hostServices { + if entry.hasPermission(info.manifest.Permissions) { + funcs, closer := entry.create(svcCtx) + hostFunctions = append(hostFunctions, funcs...) + if closer != nil { + closers = append(closers, closer) + } } } - // Register Scheduler host functions if permission is granted - if info.manifest.Permissions != nil && info.manifest.Permissions.Scheduler != nil { - service := newSchedulerService(name, m, scheduler.GetInstance()) - closers = append(closers, service) - hostFunctions = append(hostFunctions, host.RegisterSchedulerHostFunctions(service)...) - } - - // Register WebSocket host functions if permission is granted - if info.manifest.Permissions != nil && info.manifest.Permissions.Websocket != nil { - perm := info.manifest.Permissions.Websocket - service := newWebSocketService(name, m, perm.AllowedHosts) - closers = append(closers, service) - hostFunctions = append(hostFunctions, host.RegisterWebSocketHostFunctions(service)...) - } - - // Register Artwork host functions if permission is granted - if info.manifest.Permissions != nil && info.manifest.Permissions.Artwork != nil { - service := newArtworkService() - hostFunctions = append(hostFunctions, host.RegisterArtworkHostFunctions(service)...) - } - - // Register Cache host functions if permission is granted - if info.manifest.Permissions != nil && info.manifest.Permissions.Cache != nil { - service := newCacheService(name) - closers = append(closers, service) - hostFunctions = append(hostFunctions, host.RegisterCacheHostFunctions(service)...) - } - // Check if the plugin needs to be recompiled with real host functions compiled := info.compiled needsRecompile := len(pluginManifest.AllowedHosts) > 0 || len(hostFunctions) > 0 diff --git a/plugins/plugins_suite_test.go b/plugins/plugins_suite_test.go index ae6a2bc94..77a1eafc2 100644 --- a/plugins/plugins_suite_test.go +++ b/plugins/plugins_suite_test.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "net/http" "os" "os/exec" "path/filepath" @@ -113,8 +114,9 @@ func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plu // Create and start manager manager := &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), // Stub router for tests } err = manager.Start(GinkgoT().Context()) Expect(err).ToNot(HaveOccurred()) diff --git a/plugins/watcher_test.go b/plugins/watcher_test.go index 3c6b1b382..38205a3c7 100644 --- a/plugins/watcher_test.go +++ b/plugins/watcher_test.go @@ -2,6 +2,7 @@ package plugins import ( "context" + "net/http" "os" "path/filepath" @@ -135,8 +136,9 @@ var _ = Describe("Plugin Watcher", func() { dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo} autoReloadManager := &Manager{ - plugins: make(map[string]*plugin), - ds: dataStore, + plugins: make(map[string]*plugin), + ds: dataStore, + subsonicRouter: http.NotFoundHandler(), } err := autoReloadManager.Start(ctx) Expect(err).ToNot(HaveOccurred())