From 0d36b892eb295ebc44ee8094390b77671bc0c5db Mon Sep 17 00:00:00 2001 From: logikonline Date: Fri, 16 Jan 2026 22:38:01 -0500 Subject: [PATCH] Plugin entry --- modules/plugins/plugin.go | 88 +++++++++++++++ modules/plugins/registry.go | 215 ++++++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 modules/plugins/plugin.go create mode 100644 modules/plugins/registry.go diff --git a/modules/plugins/plugin.go b/modules/plugins/plugin.go new file mode 100644 index 0000000000..86dff4a330 --- /dev/null +++ b/modules/plugins/plugin.go @@ -0,0 +1,88 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package plugins + +import ( + "context" + + "github.com/go-chi/chi/v5" + "xorm.io/xorm" +) + +// Plugin defines the interface that all GitCaddy plugins must implement +type Plugin interface { + // Name returns the unique identifier for this plugin + Name() string + + // Version returns the plugin version + Version() string + + // Description returns a human-readable description + Description() string + + // Init is called when the plugin is loaded + Init(ctx context.Context) error + + // Shutdown is called when the server is shutting down + Shutdown(ctx context.Context) error +} + +// DatabasePlugin is implemented by plugins that need database tables +type DatabasePlugin interface { + Plugin + + // RegisterModels returns the models to be registered with the database + RegisterModels() []any + + // Migrate runs any database migrations for this plugin + Migrate(ctx context.Context, x *xorm.Engine) error +} + +// WebRoutesPlugin is implemented by plugins that add web UI routes +type WebRoutesPlugin interface { + Plugin + + // RegisterWebRoutes adds routes to the web UI router + RegisterWebRoutes(m chi.Router) +} + +// APIRoutesPlugin is implemented by plugins that add API routes +type APIRoutesPlugin interface { + Plugin + + // RegisterAPIRoutes adds routes to the API router + RegisterAPIRoutes(m chi.Router) +} + +// RepoRoutesPlugin is implemented by plugins that add per-repository routes +type RepoRoutesPlugin interface { + Plugin + + // RegisterRepoWebRoutes adds routes under /{owner}/{repo}/ + RegisterRepoWebRoutes(m chi.Router) + + // RegisterRepoAPIRoutes adds routes under /api/v1/repos/{owner}/{repo}/ + RegisterRepoAPIRoutes(m chi.Router) +} + +// LicensedPlugin is implemented by plugins that require a license +type LicensedPlugin interface { + Plugin + + // ValidateLicense checks if the plugin is properly licensed + ValidateLicense(ctx context.Context) error + + // LicenseInfo returns information about the current license + LicenseInfo() *LicenseInfo +} + +// LicenseInfo contains license details +type LicenseInfo struct { + Valid bool + Tier string // e.g., "solo", "pro", "team", "enterprise" + CustomerID string + ExpiresAt int64 + GracePeriod bool // true if in grace period after expiry +} + diff --git a/modules/plugins/registry.go b/modules/plugins/registry.go new file mode 100644 index 0000000000..12693b1066 --- /dev/null +++ b/modules/plugins/registry.go @@ -0,0 +1,215 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package plugins + +import ( + "context" + "sync" + + "code.gitea.io/gitea/modules/log" + + "github.com/go-chi/chi/v5" + "xorm.io/xorm" +) + +var ( + registry = make(map[string]Plugin) + registryLock sync.RWMutex +) + +// Register adds a plugin to the registry +func Register(p Plugin) { + registryLock.Lock() + defer registryLock.Unlock() + + name := p.Name() + if _, exists := registry[name]; exists { + log.Warn("Plugin %s is already registered, skipping", name) + return + } + + registry[name] = p + log.Info("Plugin registered: %s v%s", name, p.Version()) +} + +// Get returns a plugin by name +func Get(name string) Plugin { + registryLock.RLock() + defer registryLock.RUnlock() + return registry[name] +} + +// All returns all registered plugins +func All() []Plugin { + registryLock.RLock() + defer registryLock.RUnlock() + + plugins := make([]Plugin, 0, len(registry)) + for _, p := range registry { + plugins = append(plugins, p) + } + return plugins +} + +// InitAll initializes all registered plugins +func InitAll(ctx context.Context) error { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + log.Info("Initializing plugin: %s", name) + + // Check license for licensed plugins + if lp, ok := p.(LicensedPlugin); ok { + if err := lp.ValidateLicense(ctx); err != nil { + log.Warn("Plugin %s license validation failed: %v", name, err) + // Continue but features may be limited + } else { + info := lp.LicenseInfo() + if info != nil && info.Valid { + log.Info("Plugin %s licensed: tier=%s", name, info.Tier) + } + } + } + + if err := p.Init(ctx); err != nil { + log.Error("Failed to initialize plugin %s: %v", name, err) + return err + } + } + return nil +} + +// ShutdownAll shuts down all registered plugins +func ShutdownAll(ctx context.Context) { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + log.Info("Shutting down plugin: %s", name) + if err := p.Shutdown(ctx); err != nil { + log.Error("Failed to shutdown plugin %s: %v", name, err) + } + } +} + +// RegisterModels registers database models from all plugins +func RegisterModels() []any { + registryLock.RLock() + defer registryLock.RUnlock() + + var models []any + for _, p := range registry { + if dp, ok := p.(DatabasePlugin); ok { + models = append(models, dp.RegisterModels()...) + } + } + return models +} + +// MigrateAll runs migrations for all database plugins +func MigrateAll(ctx context.Context, x *xorm.Engine) error { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + if dp, ok := p.(DatabasePlugin); ok { + log.Info("Running migrations for plugin: %s", name) + if err := dp.Migrate(ctx, x); err != nil { + log.Error("Migration failed for plugin %s: %v", name, err) + return err + } + } + } + return nil +} + +// RegisterWebRoutes registers web UI routes from all plugins +func RegisterWebRoutes(m chi.Router) { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + if wp, ok := p.(WebRoutesPlugin); ok { + log.Debug("Registering web routes for plugin: %s", name) + wp.RegisterWebRoutes(m) + } + } +} + +// RegisterAPIRoutes registers API routes from all plugins +func RegisterAPIRoutes(m chi.Router) { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + if ap, ok := p.(APIRoutesPlugin); ok { + log.Debug("Registering API routes for plugin: %s", name) + ap.RegisterAPIRoutes(m) + } + } +} + +// RegisterRepoWebRoutes registers per-repository web routes from all plugins +func RegisterRepoWebRoutes(m chi.Router) { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + if rp, ok := p.(RepoRoutesPlugin); ok { + log.Debug("Registering repo web routes for plugin: %s", name) + rp.RegisterRepoWebRoutes(m) + } + } +} + +// RegisterRepoAPIRoutes registers per-repository API routes from all plugins +func RegisterRepoAPIRoutes(m chi.Router) { + registryLock.RLock() + defer registryLock.RUnlock() + + for name, p := range registry { + if rp, ok := p.(RepoRoutesPlugin); ok { + log.Debug("Registering repo API routes for plugin: %s", name) + rp.RegisterRepoAPIRoutes(m) + } + } +} + +// IsLicensed checks if a plugin is properly licensed +func IsLicensed(name string) bool { + registryLock.RLock() + defer registryLock.RUnlock() + + p, exists := registry[name] + if !exists { + return false + } + + lp, ok := p.(LicensedPlugin) + if !ok { + return true // Non-licensed plugins are always "licensed" + } + + info := lp.LicenseInfo() + return info != nil && info.Valid +} + +// GetLicenseInfo returns license info for a plugin +func GetLicenseInfo(name string) *LicenseInfo { + registryLock.RLock() + defer registryLock.RUnlock() + + p, exists := registry[name] + if !exists { + return nil + } + + lp, ok := p.(LicensedPlugin) + if !ok { + return nil + } + + return lp.LicenseInfo() +}