192 lines
6.4 KiB
Go
192 lines
6.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
var providerCommandAllowlist = map[string]struct{}{
|
|
"capabilities": {},
|
|
"transport_status": {},
|
|
"ollama_api": {},
|
|
}
|
|
|
|
func edgeCommandResponseFromNodeCommand(req *iop.EdgeCommandRequest, view NodeCommandView, err error) *iop.EdgeCommandResponse {
|
|
if err != nil {
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "error",
|
|
Error: err.Error(),
|
|
}
|
|
}
|
|
keys := make([]string, 0, len(view.Result))
|
|
for k := range view.Result {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
summaries := make([]string, 0, len(keys))
|
|
for _, k := range keys {
|
|
summaries = append(summaries, fmt.Sprintf("%s=%s", k, view.Result[k]))
|
|
}
|
|
summaryStr := fmt.Sprintf("NodeCommand %s completed: %s", req.GetParameters()["command"], strings.Join(summaries, ", "))
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "completed",
|
|
Summary: summaryStr,
|
|
}
|
|
}
|
|
|
|
func unsupportedEdgeCommand(req *iop.EdgeCommandRequest, msg string) *iop.EdgeCommandResponse {
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "unsupported",
|
|
Error: msg,
|
|
}
|
|
}
|
|
|
|
func providerCommandEvent(req *iop.EdgeCommandRequest, phase, summary string) *iop.EdgeCommandEvent {
|
|
return &iop.EdgeCommandEvent{
|
|
CommandId: req.CommandId,
|
|
Phase: phase,
|
|
Summary: summary,
|
|
OccurredAt: time.Now().UnixNano(),
|
|
}
|
|
}
|
|
|
|
// finishProviderCommand is the sole post-start terminal transition for a
|
|
// provider command. Keeping it central makes every started command observable
|
|
// as exactly one completed or failed event.
|
|
func finishProviderCommand(req *iop.EdgeCommandRequest, cmdName string, response *iop.EdgeCommandResponse, onEvent func(*iop.EdgeCommandEvent)) *iop.EdgeCommandResponse {
|
|
phase := "completed"
|
|
summary := fmt.Sprintf("Command %s completed successfully", cmdName)
|
|
if response.Status != "completed" {
|
|
phase = "failed"
|
|
summary = fmt.Sprintf("Command %s failed: %s", cmdName, response.Error)
|
|
}
|
|
onEvent(providerCommandEvent(req, phase, summary))
|
|
return response
|
|
}
|
|
|
|
func (s *Service) ExecuteCommand(ctx context.Context, req *iop.EdgeCommandRequest, onEvent func(*iop.EdgeCommandEvent)) (*iop.EdgeCommandResponse, error) {
|
|
switch req.Operation {
|
|
case "health.check":
|
|
onEvent(&iop.EdgeCommandEvent{
|
|
CommandId: req.CommandId,
|
|
Phase: "started",
|
|
Summary: "Starting health check",
|
|
OccurredAt: time.Now().UnixNano(),
|
|
})
|
|
status := s.HealthStatus()
|
|
summary := fmt.Sprintf("Edge health check OK: %d node(s), %d capability(ies)", len(status.Nodes), len(status.Capabilities))
|
|
onEvent(&iop.EdgeCommandEvent{
|
|
CommandId: req.CommandId,
|
|
Phase: "completed",
|
|
Summary: summary,
|
|
OccurredAt: time.Now().UnixNano(),
|
|
})
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "completed",
|
|
Summary: summary,
|
|
}, nil
|
|
|
|
case "node.status":
|
|
if req.TargetSelector == "" {
|
|
return nil, fmt.Errorf("target_selector is required for node.status")
|
|
}
|
|
|
|
onEvent(&iop.EdgeCommandEvent{
|
|
CommandId: req.CommandId,
|
|
Phase: "started",
|
|
Summary: fmt.Sprintf("Resolving node status for %s", req.TargetSelector),
|
|
OccurredAt: time.Now().UnixNano(),
|
|
})
|
|
|
|
entry, err := s.ResolveNode(req.TargetSelector)
|
|
if err != nil {
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "error",
|
|
Error: err.Error(),
|
|
}, nil
|
|
}
|
|
|
|
snap := NodeEntrySnapshot(entry)
|
|
onEvent(&iop.EdgeCommandEvent{
|
|
CommandId: req.CommandId,
|
|
Phase: "completed",
|
|
Summary: fmt.Sprintf("Resolved node status for %s", req.TargetSelector),
|
|
OccurredAt: time.Now().UnixNano(),
|
|
})
|
|
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "completed",
|
|
Summary: fmt.Sprintf("node ID=%s Alias=%s State=%s", snap.NodeID, snap.Alias, snap.LifecycleState),
|
|
}, nil
|
|
|
|
case "provider.command":
|
|
if strings.TrimSpace(req.GetTargetSelector()) == "" {
|
|
return nil, fmt.Errorf("target_selector is required for provider.command")
|
|
}
|
|
cmdName := req.GetParameters()["command"]
|
|
if _, ok := providerCommandAllowlist[cmdName]; !ok {
|
|
return unsupportedEdgeCommand(req, fmt.Sprintf("unsupported provider.command command %q", cmdName)), nil
|
|
}
|
|
|
|
onEvent(providerCommandEvent(req, "started", fmt.Sprintf("Routing command %s to node %s", cmdName, req.TargetSelector)))
|
|
|
|
entry, err := s.ResolveDispatchReady(req.TargetSelector)
|
|
if err != nil {
|
|
return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{
|
|
Status: "error",
|
|
Error: err.Error(),
|
|
}, onEvent), nil
|
|
}
|
|
if entry.Client == nil {
|
|
return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{
|
|
Status: "error",
|
|
Error: "node client not connected",
|
|
}, onEvent), nil
|
|
}
|
|
|
|
switch cmdName {
|
|
case "capabilities":
|
|
view, err := s.Capabilities(ctx, NodeCommandRequestSpec{
|
|
NodeRef: req.TargetSelector,
|
|
Adapter: req.GetParameters()["adapter"],
|
|
Target: req.GetParameters()["target"],
|
|
SessionID: req.GetParameters()["session_id"],
|
|
TimeoutSec: 10,
|
|
})
|
|
|
|
return finishProviderCommand(req, cmdName, edgeCommandResponseFromNodeCommand(req, view, err), onEvent), nil
|
|
|
|
case "transport_status":
|
|
view, err := s.TransportStatus(ctx, NodeCommandRequestSpec{
|
|
NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"],
|
|
Target: req.GetParameters()["target"], SessionID: req.GetParameters()["session_id"], TimeoutSec: 10,
|
|
})
|
|
return finishProviderCommand(req, cmdName, edgeCommandResponseFromNodeCommand(req, view, err), onEvent), nil
|
|
|
|
case "ollama_api":
|
|
view, err := s.OllamaAPI(ctx, OllamaAPIRequest{
|
|
NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"], Target: req.GetParameters()["target"],
|
|
Method: req.GetParameters()["method"], Path: req.GetParameters()["path"], Body: req.GetParameters()["body"], TimeoutSec: 10,
|
|
})
|
|
if err != nil {
|
|
return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{Status: "error", Error: err.Error()}, onEvent), nil
|
|
}
|
|
return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{Status: "completed", Summary: fmt.Sprintf("provider response status=%d", view.StatusCode)}, onEvent), nil
|
|
default:
|
|
// The allowlist was checked before started; retain a defensive return
|
|
// should the switch and allowlist ever diverge.
|
|
return finishProviderCommand(req, cmdName, unsupportedEdgeCommand(req, fmt.Sprintf("unsupported provider.command command %q", cmdName)), onEvent), nil
|
|
}
|
|
|
|
default:
|
|
return &iop.EdgeCommandResponse{
|
|
Status: "unsupported",
|
|
Error: fmt.Sprintf("unsupported operation %s", req.Operation),
|
|
}, nil
|
|
}
|
|
}
|