iop/apps/edge/internal/service/control_command.go
toki 4dcf6f0cf9 feat(edge): provider 리소스 승인 소유권 정렬 구현
- 노드 연결 2단계 핸드셰이크 추가 (Register→NodeReady→DispatchReady)
- ConnectionGeneration 기반 연결 세대 관리로 stale 연결 차폐
- configured 노드 catalog 기반 snapshot rebuild (Connected 상태 분리)
- provider 리스 소유권 일원화: edge가 소유권 승인·반환 전까지 대기
- 모델 대기열 승인/해제/스냅샷 서비스 구현
- 재연결 준비도 통합 테스트, 아카이브된 하위태스크 8건 포함
2026-07-22 18:10:54 +09:00

176 lines
4.8 KiB
Go

package service
import (
"context"
"fmt"
"sort"
"strings"
"time"
iop "iop/proto/gen/iop"
)
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 (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 "agent.status":
if req.TargetSelector == "" {
return nil, fmt.Errorf("target_selector is required for agent.status")
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "started",
Summary: fmt.Sprintf("Resolving agent 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 agent status for %s", req.TargetSelector),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: fmt.Sprintf("node ID=%s Alias=%s Kind=%s State=%s", snap.NodeID, snap.Alias, snap.AgentKind, snap.LifecycleState),
}, nil
case "agent.command":
cmdName := req.GetParameters()["command"]
if cmdName == "" {
return unsupportedEdgeCommand(req, "missing command parameter for agent.command"), nil
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "started",
Summary: fmt.Sprintf("Routing command %s to node %s", cmdName, req.TargetSelector),
OccurredAt: time.Now().UnixNano(),
})
entry, err := s.ResolveDispatchReady(req.TargetSelector)
if err != nil {
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "failed",
Summary: fmt.Sprintf("Command %s failed: %v", cmdName, err),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "error",
Error: err.Error(),
}, nil
}
if entry.Client == nil {
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "failed",
Summary: fmt.Sprintf("Command %s failed: node client not connected", cmdName),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "error",
Error: "node client not connected",
}, 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,
})
var phase string
var summary string
if err != nil {
phase = "failed"
summary = fmt.Sprintf("Command %s failed: %v", cmdName, err)
} else {
phase = "completed"
summary = fmt.Sprintf("Command %s completed successfully", cmdName)
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: phase,
Summary: summary,
OccurredAt: time.Now().UnixNano(),
})
return edgeCommandResponseFromNodeCommand(req, view, err), nil
default:
return unsupportedEdgeCommand(req, fmt.Sprintf("unsupported agent.command command %q", cmdName)), nil
}
default:
return &iop.EdgeCommandResponse{
Status: "unsupported",
Error: fmt.Sprintf("unsupported operation %s", req.Operation),
}, nil
}
}