diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 33e6b7f9e..e6f354157 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -4,12 +4,15 @@ import ( "context" "fmt" "net/http" + "os" + "path/filepath" "sort" "strings" "time" "github.com/gin-gonic/gin" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" + "github.com/seaweedfs/seaweedfs/weed/admin/plugin" "github.com/seaweedfs/seaweedfs/weed/cluster" "github.com/seaweedfs/seaweedfs/weed/credential" "github.com/seaweedfs/seaweedfs/weed/glog" @@ -254,10 +257,24 @@ func (s *AdminServer) GetCredentialManager() *credential.CredentialManager { // initPluginManager initializes the plugin manager func (s *AdminServer) initPluginManager(dataDir string) { - // For now, keep pluginManager as interface{} to avoid circular imports - // This will be set later when handlers are initialized - // We'll check if it's nil when needed - glog.V(1).Infof("Plugin manager placeholder initialized") + // Create plugin configuration directory if it doesn't exist + pluginConfigDir := filepath.Join(dataDir, "plugins") + if err := os.MkdirAll(pluginConfigDir, 0755); err != nil { + glog.Warningf("Failed to create plugin config directory: %v", err) + return + } + + // Create plugin manager with default configuration + config := plugin.DefaultManagerConfig(pluginConfigDir) + pm, err := plugin.NewManager(config) + if err != nil { + glog.Warningf("Failed to initialize plugin manager: %v", err) + return + } + + // Store the plugin manager + s.pluginManager = pm + glog.Infof("Plugin manager initialized successfully") } // GetPluginManager returns the plugin manager diff --git a/weed/admin/dash/worker_grpc_server.go b/weed/admin/dash/worker_grpc_server.go index 015bd4215..37dbbfed2 100644 --- a/weed/admin/dash/worker_grpc_server.go +++ b/weed/admin/dash/worker_grpc_server.go @@ -9,8 +9,10 @@ import ( "time" "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" + "github.com/seaweedfs/seaweedfs/weed/admin/plugin" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb" + "github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb" "github.com/seaweedfs/seaweedfs/weed/pb/worker_pb" "github.com/seaweedfs/seaweedfs/weed/security" "github.com/seaweedfs/seaweedfs/weed/util" @@ -94,6 +96,19 @@ func (s *WorkerGrpcServer) StartWithTLS(port int) error { worker_pb.RegisterWorkerServiceServer(grpcServer, s) + // Register plugin service if plugin manager is available + if s.adminServer.GetPluginManager() != nil { + // Cast the interface{} to *plugin.Manager + if pm, ok := s.adminServer.GetPluginManager().(*plugin.Manager); ok { + if pluginGrpcServer := pm.GetGRPCServer(); pluginGrpcServer != nil { + plugin_pb.RegisterPluginServiceServer(grpcServer, pluginGrpcServer) + plugin_pb.RegisterAdminQueryServiceServer(grpcServer, pluginGrpcServer) + plugin_pb.RegisterAdminCommandServiceServer(grpcServer, pluginGrpcServer) + glog.Infof("Registered plugin services on worker gRPC server") + } + } + } + s.grpcServer = grpcServer s.listener = listener s.running = true diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 04c31c060..ff86a9c0e 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/admin/plugin" "github.com/seaweedfs/seaweedfs/weed/admin/view/app" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" @@ -39,14 +40,14 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { maintenanceHandlers := NewMaintenanceHandlers(adminServer) mqHandlers := NewMessageQueueHandlers(adminServer) serviceAccountHandlers := NewServiceAccountHandlers(adminServer) - + // Get plugin manager from admin server (may be nil) var pluginMgr interface{} if pm := adminServer.GetPluginManager(); pm != nil { pluginMgr = pm } pluginHandlers := NewPluginHandlers(adminServer, pluginMgr) - + return &AdminHandlers{ adminServer: adminServer, authHandlers: authHandlers, @@ -270,13 +271,13 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, pluginApi := api.Group("/plugin") { pluginApi.GET("/list", h.pluginHandlers.ListPluginsAPI) - pluginApi.GET("/jobs/:type", h.pluginHandlers.ListJobsAPI) + pluginApi.GET("/jobs/by-type/:type", h.pluginHandlers.ListJobsAPI) pluginApi.GET("/config/:type", h.pluginHandlers.GetConfigAPI) pluginApi.POST("/config/:type/apply", dash.RequireWriteAccess(), h.pluginHandlers.SaveConfigAPI) pluginApi.GET("/detection/history/:type", h.pluginHandlers.GetDetectionHistoryAPI) pluginApi.GET("/execution/history/:type", h.pluginHandlers.GetExecutionHistoryAPI) - pluginApi.POST("/jobs/:type/trigger-detection", dash.RequireWriteAccess(), h.pluginHandlers.TriggerDetectionAPI) - pluginApi.POST("/jobs/:id/cancel", dash.RequireWriteAccess(), h.pluginHandlers.CancelJobAPI) + pluginApi.POST("/trigger-detection/:type", dash.RequireWriteAccess(), h.pluginHandlers.TriggerDetectionAPI) + pluginApi.POST("/cancel-job/:id", dash.RequireWriteAccess(), h.pluginHandlers.CancelJobAPI) } } } else { @@ -320,6 +321,11 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, r.GET("/mq/topics", h.mqHandlers.ShowTopics) r.GET("/mq/topics/:namespace/:topic", h.mqHandlers.ShowTopicDetails) + // Plugin management routes + r.GET("/plugins", h.ShowPlugins) + r.GET("/plugins/jobs/:jobType", h.ShowPluginJobs) + r.GET("/plugins/config/:jobType", h.ShowPluginConfig) + // Maintenance system routes r.GET("/maintenance", h.maintenanceHandlers.ShowMaintenanceQueue) r.GET("/maintenance/workers", h.maintenanceHandlers.ShowMaintenanceWorkers) @@ -450,6 +456,19 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, mqApi.POST("/topics/retention/update", h.mqHandlers.UpdateTopicRetentionAPI) mqApi.POST("/retention/purge", h.adminServer.TriggerTopicRetentionPurgeAPI) } + + // Plugin API routes + pluginApi := api.Group("/plugin") + { + pluginApi.GET("/list", h.pluginHandlers.ListPluginsAPI) + pluginApi.GET("/jobs/by-type/:type", h.pluginHandlers.ListJobsAPI) + pluginApi.GET("/config/:type", h.pluginHandlers.GetConfigAPI) + pluginApi.POST("/config/:type/apply", h.pluginHandlers.SaveConfigAPI) + pluginApi.GET("/detection/history/:type", h.pluginHandlers.GetDetectionHistoryAPI) + pluginApi.GET("/execution/history/:type", h.pluginHandlers.GetExecutionHistoryAPI) + pluginApi.POST("/trigger-detection/:type", h.pluginHandlers.TriggerDetectionAPI) + pluginApi.POST("/cancel-job/:id", h.pluginHandlers.CancelJobAPI) + } } } } @@ -700,19 +719,52 @@ func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData { // ShowPlugins displays the plugins overview page func (h *AdminHandlers) ShowPlugins(c *gin.Context) { - plugins := []interface{}{} + plugins := []map[string]interface{}{} jobTypes := make(map[string]interface{}) - + // Get plugin manager from server if pm := h.adminServer.GetPluginManager(); pm != nil { - // TODO: Get actual plugins from plugin manager + // Cast to *plugin.Manager + if pluginMgr, ok := pm.(*plugin.Manager); ok { + // Get list of connected plugins + connectedPlugins := pluginMgr.ListPlugins(false) + for _, p := range connectedPlugins { + plugins = append(plugins, map[string]interface{}{ + "id": p.ID, + "name": p.Name, + "version": p.Version, + "status": p.Status, + "capabilities": p.Capabilities, + "activeJobs": p.ActiveJobs, + "completedJobs": p.CompletedJobs, + "failedJobs": p.FailedJobs, + "connectedAt": p.ConnectedAt, + "lastHeartbeat": p.LastHeartbeat, + }) + + // Build job types map + for _, cap := range p.Capabilities { + if _, exists := jobTypes[cap]; !exists { + jobTypes[cap] = map[string]interface{}{ + "type": cap, + "description": cap, + "pluginCount": 0, + } + } + // Increment plugin count for this capability + if capData, ok := jobTypes[cap].(map[string]interface{}); ok { + capData["pluginCount"] = capData["pluginCount"].(int) + 1 + } + } + } + } } - + component := app.PluginsOverview(app.PluginsPageData{ Plugins: plugins, JobTypes: jobTypes, }) - + htmlContent := layout.Layout(c, component) htmlContent.Render(c.Request.Context(), c.Writer) } @@ -722,13 +774,13 @@ func (h *AdminHandlers) ShowPluginJobs(c *gin.Context) { jobType := c.Param("jobType") jobs := []interface{}{} stateFilter := c.Query("state") - + component := app.PluginJobsMonitoring(app.PluginJobsPageData{ JobType: jobType, Jobs: jobs, StateFilter: stateFilter, }) - + htmlContent := layout.Layout(c, component) htmlContent.Render(c.Request.Context(), c.Writer) } @@ -740,15 +792,15 @@ func (h *AdminHandlers) ShowPluginConfig(c *gin.Context) { if activeTab == "" { activeTab = "config" } - + component := app.PluginConfiguration(app.PluginConfigPageData{ - JobType: jobType, - Config: app.JobTypeConfig{}, - DetectionHistory: []interface{}{}, - ExecutionHistory: []interface{}{}, - ActiveTab: activeTab, + JobType: jobType, + Config: app.JobTypeConfig{}, + DetectionHistory: []interface{}{}, + ExecutionHistory: []interface{}{}, + ActiveTab: activeTab, }) - + htmlContent := layout.Layout(c, component) htmlContent.Render(c.Request.Context(), c.Writer) } diff --git a/weed/admin/plugin/grpc_server.go b/weed/admin/plugin/grpc_server.go index e401370b6..acf51aaa3 100644 --- a/weed/admin/plugin/grpc_server.go +++ b/weed/admin/plugin/grpc_server.go @@ -11,12 +11,12 @@ import ( // GRPCServer implements the plugin service gRPC handlers type GRPCServer struct { - mu sync.RWMutex - registry *Registry - queue *JobQueue - dispatcher *Dispatcher - configMgr *ConfigManager - streamMu sync.RWMutex + mu sync.RWMutex + registry *Registry + queue *JobQueue + dispatcher *Dispatcher + configMgr *ConfigManager + streamMu sync.RWMutex activeStreams map[string][]chan interface{} plugin_pb.UnimplementedPluginServiceServer plugin_pb.UnimplementedAdminQueryServiceServer @@ -78,17 +78,17 @@ func (gs *GRPCServer) Connect(ctx context.Context, req *plugin_pb.PluginConnectR // Build response pbConfig := &plugin_pb.PluginConfig{ - PluginId: config.PluginID, - Properties: config.Properties, - MaxRetries: int32(config.MaxRetries), - Environment: config.Environment, + PluginId: config.PluginID, + Properties: config.Properties, + MaxRetries: int32(config.MaxRetries), + Environment: config.Environment, } response := &plugin_pb.PluginConnectResponse{ - Success: true, - Message: "Plugin registered successfully", - MasterId: "master-1", - Config: pbConfig, + Success: true, + Message: "Plugin registered successfully", + MasterId: "master-1", + Config: pbConfig, AssignedTypes: req.Capabilities, } @@ -102,8 +102,8 @@ func (gs *GRPCServer) ExecuteJob(ctx context.Context, req *plugin_pb.ExecuteJobR } response := &plugin_pb.ExecuteJobResponse{ - JobId: req.JobId, - Status: plugin_pb.ExecutionStatus_EXECUTION_STATUS_ACCEPTED, + JobId: req.JobId, + Status: plugin_pb.ExecutionStatus_EXECUTION_STATUS_ACCEPTED, Message: "Job accepted for execution", } @@ -148,10 +148,10 @@ func (gs *GRPCServer) GetConfig(ctx context.Context, req *plugin_pb.GetConfigReq } pbConfig := &plugin_pb.PluginConfig{ - PluginId: config.PluginID, - Properties: config.Properties, - MaxRetries: int32(config.MaxRetries), - Environment: config.Environment, + PluginId: config.PluginID, + Properties: config.Properties, + MaxRetries: int32(config.MaxRetries), + Environment: config.Environment, } response := &plugin_pb.GetConfigResponse{ @@ -169,7 +169,7 @@ func (gs *GRPCServer) SubmitResult(ctx context.Context, req *plugin_pb.JobResult } actions := []string{} - + // Process results based on job status switch req.Status { case plugin_pb.ExecutionStatus_EXECUTION_STATUS_COMPLETED: @@ -203,16 +203,16 @@ func (gs *GRPCServer) GetPluginStats(ctx context.Context, req *plugin_pb.GetPlug for _, plugin := range plugins { stat := &plugin_pb.PluginStats{ - PluginId: plugin.ID, - Status: plugin.Status, - ActiveJobs: int32(plugin.ActiveJobs), - CompletedJobs: int32(plugin.CompletedJobs), - FailedJobs: int32(plugin.FailedJobs), - TotalDetections: plugin.TotalDetections, - AvgExecutionTimeMs: float32(plugin.AvgExecutionTimeMs), - CpuUsagePercent: float32(plugin.CPUUsagePercent), - MemoryUsageBytes: plugin.MemoryUsageBytes, - UptimeSeconds: int32(time.Since(plugin.ConnectedAt).Seconds()), + PluginId: plugin.ID, + Status: plugin.Status, + ActiveJobs: int32(plugin.ActiveJobs), + CompletedJobs: int32(plugin.CompletedJobs), + FailedJobs: int32(plugin.FailedJobs), + TotalDetections: plugin.TotalDetections, + AvgExecutionTimeMs: float32(plugin.AvgExecutionTimeMs), + CpuUsagePercent: float32(plugin.CPUUsagePercent), + MemoryUsageBytes: plugin.MemoryUsageBytes, + UptimeSeconds: int32(time.Since(plugin.ConnectedAt).Seconds()), } response.Stats = append(response.Stats, stat) } @@ -249,14 +249,14 @@ func (gs *GRPCServer) ListPlugins(ctx context.Context, req *plugin_pb.ListPlugin } info := &plugin_pb.PluginInfo{ - PluginId: plugin.ID, - Name: plugin.Name, - Version: plugin.Version, - Status: plugin.Status, - Capabilities: plugin.Capabilities, + PluginId: plugin.ID, + Name: plugin.Name, + Version: plugin.Version, + Status: plugin.Status, + Capabilities: plugin.Capabilities, MaxConcurrentJobs: int32(plugin.MaxConcurrentJobs), - ActiveJobs: int32(plugin.ActiveJobs), - Metadata: plugin.Metadata, + ActiveJobs: int32(plugin.ActiveJobs), + Metadata: plugin.Metadata, } response.Plugins = append(response.Plugins, info) } diff --git a/weed/admin/view/app/plugins.templ b/weed/admin/view/app/plugins.templ index 65e2999df..6d4066025 100644 --- a/weed/admin/view/app/plugins.templ +++ b/weed/admin/view/app/plugins.templ @@ -1,8 +1,10 @@ package app +import "fmt" + type PluginsPageData struct { -Plugins []interface{} -JobTypes map[string]interface{} + Plugins []map[string]interface{} + JobTypes map[string]interface{} } templ PluginsOverview(data PluginsPageData) { @@ -26,7 +28,7 @@ templ PluginsOverview(data PluginsPageData) {
| Plugin | +Plugin ID | +Name | Status | +Version | +Capabilities | Actions |
|---|---|---|---|---|---|---|
{ p["id"].(string) } |
+{ p["name"].(string) } |
-Plugin -v1.0 +if p["status"].(string) == "CONNECTED" { +Connected +} else { +{ p["status"].(string) } +} + |
+{ p["version"].(string) } | ++for _, cap := range p["capabilities"].([]string) { +{ cap } +} | -Healthy - | --Config +Jobs +Config |
| Plugin | Status | Actions |
|---|
| Plugin ID | Name | Status | Version | Capabilities | Actions |
|---|---|---|---|---|---|
| Plugin v1.0 | Healthy | Config | |||
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(p["id"].(string))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 65, Col: 28}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(p["name"].(string)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 66, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if p["status"].(string) == "CONNECTED" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Connected") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(p["status"].(string)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 71, Col: 53} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(p["version"].(string)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 74, Col: 27} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " | ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, cap := range p["capabilities"].([]string) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(cap) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 77, Col: 38} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " | Jobs Config |