From 8704952d6fdb07915d9dd8d9e279761e2f0374ee Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 19 Sep 2025 15:24:08 +0900 Subject: [PATCH] Add usescases to handle test plugins --- .../controller/plugin_test_controller.go | 78 +++++++++++++++++++ internal/api-admin/route/plugin.go | 52 +++++++++++++ internal/api-admin/route/route.go | 1 + internal/app/app.go | 13 +++- internal/app/plugins.go | 15 ++-- .../usecase/plugin/plugin_test_usecase.go | 52 +++++++++++++ model/plugin.go | 13 +++- model/usecase.go | 1 + plugins/matrix/test.go | 2 +- 9 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 internal/api-admin/controller/plugin_test_controller.go create mode 100644 internal/api-admin/route/plugin.go create mode 100644 internal/usecase/plugin/plugin_test_usecase.go diff --git a/internal/api-admin/controller/plugin_test_controller.go b/internal/api-admin/controller/plugin_test_controller.go new file mode 100644 index 00000000..4a976c0c --- /dev/null +++ b/internal/api-admin/controller/plugin_test_controller.go @@ -0,0 +1,78 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2020-2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "git.happydns.org/happyDomain/internal/api/middleware" + "git.happydns.org/happyDomain/model" +) + +type TestPluginController struct { + testPluginService happydns.TestPluginUsecase +} + +func NewTestPluginController(testPluginService happydns.TestPluginUsecase) *TestPluginController { + return &TestPluginController{ + testPluginService, + } +} + +func (uc *TestPluginController) TestPluginHandler(c *gin.Context) { + pname := c.Param("pname") + + plugin, err := uc.testPluginService.GetTestPlugin(pname) + if err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, happydns.ErrorResponse{Message: "Plugin not found"}) + return + } + + c.Set("plugin", plugin) + + c.Next() +} + +func (uc *TestPluginController) ListTestPlugins(c *gin.Context) { + plugins, err := uc.testPluginService.ListTestPlugins() + if err != nil { + middleware.ErrorResponse(c, http.StatusInternalServerError, err) + return + } + + ret := map[string]happydns.PluginVersionInfo{} + + for _, p := range plugins { + pnames := p.PluginEnvName() + ret[pnames[0]] = p.Version() + } + + happydns.ApiResponse(c, ret, nil) +} + +func (uc *TestPluginController) GetTestPluginStatus(c *gin.Context) { + plugin := c.MustGet("plugin").(happydns.TestPlugin) + + c.JSON(http.StatusOK, plugin.Version()) +} diff --git a/internal/api-admin/route/plugin.go b/internal/api-admin/route/plugin.go new file mode 100644 index 00000000..107cba02 --- /dev/null +++ b/internal/api-admin/route/plugin.go @@ -0,0 +1,52 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2020-2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package route + +import ( + "github.com/gin-gonic/gin" + + "git.happydns.org/happyDomain/internal/api-admin/controller" + "git.happydns.org/happyDomain/internal/storage" + "git.happydns.org/happyDomain/model" +) + +func declarePluginsRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseDependancies, store storage.Storage) { + apiPluginsRoutes := router.Group("/plugins") + declareTestPluginsRoutes(apiPluginsRoutes, dependancies) +} + +func declareTestPluginsRoutes(router *gin.RouterGroup, dependancies happydns.UsecaseDependancies) { + tpc := controller.NewTestPluginController(dependancies.TestPluginUsecase()) + + apiTestPluginsRoutes := router.Group("/tests") + + apiTestPluginsRoutes.GET("", tpc.ListTestPlugins) + + apiTestPluginRoutes := apiTestPluginsRoutes.Group("/:pname") + apiTestPluginRoutes.Use(tpc.TestPluginHandler) + + apiTestPluginRoutes.GET("", tpc.GetTestPluginStatus) + //apiTestPluginRoutes.POST("", tpc.ChangeTestPluginStatus) + + //apiTestPluginRoutes.GET("/options", tpc.ListTestPluginOptions) + //apiTestPluginRoutes.PUT("/options", tpc.ChangeTestPluginOptions) +} diff --git a/internal/api-admin/route/route.go b/internal/api-admin/route/route.go index fe387b93..9c8f168e 100644 --- a/internal/api-admin/route/route.go +++ b/internal/api-admin/route/route.go @@ -34,6 +34,7 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.Engine, s storage.Storage, declareBackupRoutes(cfg, apiRoutes, s) declareDomainRoutes(apiRoutes, dependancies, s) + declarePluginsRoutes(apiRoutes, dependancies, s) declareProviderRoutes(apiRoutes, dependancies, s) declareSessionsRoutes(cfg, apiRoutes, s) declareUserAuthsRoutes(apiRoutes, dependancies, s) diff --git a/internal/app/app.go b/internal/app/app.go index 854f130e..38da2d20 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -40,6 +40,7 @@ import ( domainUC "git.happydns.org/happyDomain/internal/usecase/domain" domainlogUC "git.happydns.org/happyDomain/internal/usecase/domain_log" "git.happydns.org/happyDomain/internal/usecase/orchestrator" + pluginUC "git.happydns.org/happyDomain/internal/usecase/plugin" providerUC "git.happydns.org/happyDomain/internal/usecase/provider" serviceUC "git.happydns.org/happyDomain/internal/usecase/service" sessionUC "git.happydns.org/happyDomain/internal/usecase/session" @@ -63,6 +64,7 @@ type Usecases struct { session happydns.SessionUsecase service happydns.ServiceUsecase serviceSpecs happydns.ServiceSpecsUsecase + testPlugin happydns.TestPluginUsecase user happydns.UserUsecase zone happydns.ZoneUsecase zoneService happydns.ZoneServiceUsecase @@ -77,7 +79,7 @@ type App struct { router *gin.Engine srv *http.Server insights *insightsCollector - plugins happydns.PluginManger + plugins happydns.PluginManager store storage.Storage usecases Usecases } @@ -138,6 +140,10 @@ func (a *App) SessionUsecase() happydns.SessionUsecase { return a.usecases.session } +func (a *App) TestPluginUsecase() happydns.TestPluginUsecase { + return a.usecases.testPlugin +} + func (a *App) UserUsecase() happydns.UserUsecase { return a.usecases.user } @@ -167,9 +173,9 @@ func NewApp(cfg *happydns.Options) *App { app.initStorageEngine() app.initNewsletter() app.initInsights() + app.initPlugins() app.initUsecases() app.setupRouter() - app.initPlugins() return app } @@ -182,9 +188,9 @@ func NewAppWithStorage(cfg *happydns.Options, store storage.Storage) *App { app.initMailer() app.initNewsletter() + app.initPlugins() app.initUsecases() app.setupRouter() - app.initPlugins() return app } @@ -273,6 +279,7 @@ func (app *App) initUsecases() { app.usecases.authUser = authUserService app.usecases.resolver = usecase.NewResolverUsecase(app.cfg) app.usecases.session = sessionService + app.usecases.testPlugin = pluginUC.NewTestPluginUsecase(app.cfg, app.plugins) app.usecases.orchestrator = orchestrator.NewOrchestrator( domainLogService, diff --git a/internal/app/plugins.go b/internal/app/plugins.go index fd61b0f1..f3c0340a 100644 --- a/internal/app/plugins.go +++ b/internal/app/plugins.go @@ -33,9 +33,10 @@ import ( ) func (a *App) initPlugins() error { - a.plugins = PluginManger{ + manager := PluginManger{ testsIdx: map[string]happydns.TestPlugin{}, } + a.plugins = &manager var ret error @@ -53,7 +54,7 @@ func (a *App) initPlugins() error { fname := path.Join(directory, file.Name()) - err = a.plugins.loadPlugin(fname) + err = manager.loadPlugin(fname) if err != nil { ret = errors.Join(ret, fmt.Errorf("unable to load plugin %q: %w", fname, err)) } @@ -84,27 +85,27 @@ func (m *PluginManger) loadPlugin(fname string) error { return err } - m.plugins = append(m.plugins, myplugin) + m.tests = append(m.tests, myplugin) // Index the plugin by its names pluginNames := myplugin.PluginEnvName() for _, name := range pluginNames { - if p, exists := m.pluginsIdx[name]; exists { + if p, exists := m.testsIdx[name]; exists { log.Printf("Plugin name conflict: the plugin at %q tries to register the name %q but it's already registered by %q", fname, name, p.Version().Name) continue } - m.pluginsIdx[name] = myplugin + m.testsIdx[name] = myplugin } log.Printf("Plugin %s loaded (version %s)", myplugin.Version().Name, myplugin.Version().Version) return nil } -func (m *PluginManger) GetTestPlugins() ([]happydns.TestPlugin, error) { +func (m *PluginManger) GetTestPlugins() []happydns.TestPlugin { return m.tests } -func (m *PluginManger) GetTestPluginsIndex() (map[string]happydns.TestPlugin, error) { +func (m *PluginManger) GetTestPluginsIndex() map[string]happydns.TestPlugin { return m.testsIdx } diff --git a/internal/usecase/plugin/plugin_test_usecase.go b/internal/usecase/plugin/plugin_test_usecase.go new file mode 100644 index 00000000..5c56e775 --- /dev/null +++ b/internal/usecase/plugin/plugin_test_usecase.go @@ -0,0 +1,52 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2020-2025 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugin + +import ( + "fmt" + + "git.happydns.org/happyDomain/model" +) + +type testPluginUsecase struct { + config *happydns.Options + manager happydns.PluginManager +} + +func NewTestPluginUsecase(cfg *happydns.Options, manager happydns.PluginManager) happydns.TestPluginUsecase { + return &testPluginUsecase{ + config: cfg, + manager: manager, + } +} + +func (tu *testPluginUsecase) GetTestPlugin(pname string) (happydns.TestPlugin, error) { + if plugin, ok := tu.manager.GetTestPluginsIndex()[pname]; !ok { + return nil, fmt.Errorf("unable to find plugin named %q", pname) + } else { + return plugin, nil + } +} + +func (tu *testPluginUsecase) ListTestPlugins() ([]happydns.TestPlugin, error) { + return tu.manager.GetTestPlugins(), nil +} diff --git a/model/plugin.go b/model/plugin.go index 32a48d85..fdbb8527 100644 --- a/model/plugin.go +++ b/model/plugin.go @@ -30,11 +30,13 @@ const ( type PluginResultStatus int +type PluginOptions map[string]interface{} + type TestPlugin interface { PluginEnvName() []string Version() PluginVersionInfo - RunTest(options map[string]interface{}, meta map[string]string) (*PluginResult, error) + RunTest(options PluginOptions, meta map[string]string) (*PluginResult, error) } type PluginVersionInfo struct { @@ -57,6 +59,11 @@ type PluginResult struct { } type PluginManager interface { - GetTestPlugins() ([]TestPlugin, error) - GetTestPluginsIndex() (map[string]TestPlugin, error) + GetTestPlugins() []TestPlugin + GetTestPluginsIndex() map[string]TestPlugin +} + +type TestPluginUsecase interface { + GetTestPlugin(string) (TestPlugin, error) + ListTestPlugins() ([]TestPlugin, error) } diff --git a/model/usecase.go b/model/usecase.go index 958843d3..2ee4392f 100644 --- a/model/usecase.go +++ b/model/usecase.go @@ -34,6 +34,7 @@ type UsecaseDependancies interface { ServiceUsecase() ServiceUsecase ServiceSpecsUsecase() ServiceSpecsUsecase SessionUsecase() SessionUsecase + TestPluginUsecase() TestPluginUsecase UserUsecase() UserUsecase ZoneCorrectionApplierUsecase() ZoneCorrectionApplierUsecase ZoneImporterUsecase() ZoneImporterUsecase diff --git a/plugins/matrix/test.go b/plugins/matrix/test.go index ad94ef89..c43ffc5d 100644 --- a/plugins/matrix/test.go +++ b/plugins/matrix/test.go @@ -54,7 +54,7 @@ type FederationTesterResponse struct { FederationOK bool `json:"FederationOK"` } -func (p *MatrixTester) RunTest(options map[string]interface{}, meta map[string]string) (*happydns.PluginResult, error) { +func (p *MatrixTester) RunTest(options happydns.PluginOptions, meta map[string]string) (*happydns.PluginResult, error) { var domain string if dn, ok := options["domain"]; ok {