Pulse/internal/api/action_executor.go
2026-06-13 14:11:32 +01:00

124 lines
3.8 KiB
Go

package api
import (
"context"
"fmt"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
type actionAgentCommander interface {
ExecuteCommand(ctx context.Context, agentID string, cmd agentexec.ExecuteCommandPayload) (*agentexec.CommandResultPayload, error)
GetAgentForHost(hostname string) (string, bool)
IsAgentConnected(agentID string) bool
}
type actionHandlerProvider interface {
ActionHandlerNames() []string
}
type routedActionExecutor struct {
resources *ResourceHandlers
byHandler map[string]ActionExecutor
}
func newRoutedActionExecutor(resources *ResourceHandlers, executors ...ActionExecutor) ActionExecutor {
if resources == nil {
return nil
}
routed := routedActionExecutor{
resources: resources,
byHandler: map[string]ActionExecutor{},
}
for _, executor := range executors {
if executor == nil {
continue
}
provider, ok := executor.(actionHandlerProvider)
if !ok {
continue
}
for _, handler := range provider.ActionHandlerNames() {
handler = strings.TrimSpace(handler)
if handler != "" {
routed.byHandler[handler] = executor
}
}
}
if len(routed.byHandler) == 0 {
return nil
}
return routed
}
func (e routedActionExecutor) ExecuteAction(ctx context.Context, record unified.ActionAuditRecord) (*unified.ExecutionResult, error) {
normalized, err := unified.NormalizeActionAuditRecord(record)
if err != nil {
return nil, err
}
executor, err := e.executorForAction(ctx, normalized.Request)
if err != nil {
return nil, err
}
return executor.ExecuteAction(ctx, normalized)
}
func (e routedActionExecutor) CheckActionAvailable(ctx context.Context, req unified.ActionRequest, resource unified.Resource) unified.ResourceActionReadiness {
capability, ok := resourceCapabilityByName(resource.Capabilities, req.CapabilityName)
if !ok || strings.TrimSpace(capability.InternalHandler) == "" {
return unified.ResourceActionReadiness{}
}
executor := e.byHandler[strings.TrimSpace(capability.InternalHandler)]
if executor == nil {
return unified.ResourceActionReadiness{
Name: strings.TrimSpace(req.CapabilityName),
Available: false,
ReasonCode: "unsupported_handler",
Reason: "This action is not routed through a supported executor.",
}
}
checker, ok := executor.(ActionAvailabilityChecker)
if !ok {
return unified.ResourceActionReadiness{}
}
return checker.CheckActionAvailable(ctx, req, resource)
}
func (e routedActionExecutor) executorForAction(ctx context.Context, req unified.ActionRequest) (ActionExecutor, error) {
if e.resources == nil {
return nil, fmt.Errorf("resource handler unavailable")
}
registry, err := e.resources.buildRegistry(GetOrgID(ctx))
if err != nil {
return nil, err
}
resource, ok := registry.Get(req.ResourceID)
if !ok || resource == nil {
return nil, fmt.Errorf("resource %q is no longer present", req.ResourceID)
}
capability, ok := resourceCapabilityByName(resource.Capabilities, req.CapabilityName)
if !ok {
return nil, fmt.Errorf("resource %q does not currently advertise %s capability", req.ResourceID, req.CapabilityName)
}
handler := strings.TrimSpace(capability.InternalHandler)
if handler == "" {
return nil, fmt.Errorf("resource %q capability %s has no executor handler", req.ResourceID, req.CapabilityName)
}
executor := e.byHandler[handler]
if executor == nil {
return nil, fmt.Errorf("resource %q capability %s is routed through unsupported handler %q", req.ResourceID, req.CapabilityName, handler)
}
return executor, nil
}
func resourceCapabilityByName(capabilities []unified.ResourceCapability, name string) (unified.ResourceCapability, bool) {
name = strings.TrimSpace(name)
for _, capability := range capabilities {
if strings.TrimSpace(capability.Name) == name {
return capability, true
}
}
return unified.ResourceCapability{}, false
}