iop/apps/control-plane/cmd/control-plane/http_views.go
toki 3f8da3354c feat: provider catalog device status milestone completion and catalog view implementation
- Add provider catalog device status milestone tracking
- Implement catalog status HTTP endpoints
- Add client catalog view implementation
- Update control-plane HTTP views and tests
- Update edge cmd nodes and tests
- Update roadmap and phase documentation
2026-06-20 19:51:45 +09:00

386 lines
13 KiB
Go

package main
import (
"time"
iop "iop/proto/gen/iop"
"iop/apps/control-plane/internal/wire"
)
// ── DTO structs ──────────────────────────────────────────────────────────────
type edgeRegistryResponse struct {
Edges []edgeRegistryView `json:"edges"`
}
type edgeRegistryView struct {
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name"`
Version string `json:"version"`
Capabilities []string `json:"capabilities"`
Protocol string `json:"protocol"`
Connected bool `json:"connected"`
LastSeen time.Time `json:"last_seen"`
}
type edgeNodeEventsResponse struct {
EdgeID string `json:"edge_id"`
Events []edgeNodeEventView `json:"events"`
}
type edgeNodeEventView struct {
EdgeID string `json:"edge_id"`
EventID string `json:"event_id"`
Type string `json:"type"`
Source string `json:"source"`
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Reason string `json:"reason"`
Timestamp time.Time `json:"timestamp"`
ReceivedAt time.Time `json:"received_at"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type edgeStatusResponseView struct {
RequestID string `json:"request_id"`
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name"`
ObservedTimeUnixNano int64 `json:"observed_time_unix_nano"`
Nodes []edgeNodeSnapshotView `json:"nodes"`
Capabilities []edgeCapabilitySummaryView `json:"capabilities,omitempty"`
DomainAgents []edgeDomainAgentSummaryView `json:"domain_agents,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
Error string `json:"error,omitempty"`
}
type edgeCapabilitySummaryView struct {
Kind string `json:"kind"`
Available bool `json:"available"`
Status string `json:"status,omitempty"`
Summary string `json:"summary,omitempty"`
}
type edgeDomainAgentSummaryView struct {
AgentKind string `json:"agent_kind"`
Available bool `json:"available"`
LifecycleState string `json:"lifecycle_state,omitempty"`
ActiveCommandID string `json:"active_command_id,omitempty"`
Summary string `json:"summary,omitempty"`
}
// edgeCommandRequestBody is the JSON body for POST /edges/{id}/commands and
// POST /fleet/commands. It carries the surface-neutral command intent only.
type edgeCommandRequestBody struct {
Operation string `json:"operation"`
TargetSelector string `json:"target_selector,omitempty"`
Parameters map[string]string `json:"parameters,omitempty"`
}
type edgeCommandResponseView struct {
RequestID string `json:"request_id,omitempty"`
CommandID string `json:"command_id"`
EdgeID string `json:"edge_id"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Error string `json:"error,omitempty"`
}
type edgeOperationsResponse struct {
EdgeID string `json:"edge_id"`
Operations []edgeCommandRecordView `json:"operations"`
}
type edgeCommandRecordView struct {
EdgeID string `json:"edge_id"`
CommandID string `json:"command_id"`
Operation string `json:"operation,omitempty"`
TargetSelector string `json:"target_selector,omitempty"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Events []edgeCommandEventView `json:"events,omitempty"`
}
type edgeCommandEventView struct {
Phase string `json:"phase,omitempty"`
Summary string `json:"summary,omitempty"`
OccurredAt time.Time `json:"occurred_at,omitempty"`
ReceivedAt time.Time `json:"received_at"`
}
type fleetStatusResponse struct {
GeneratedAt time.Time `json:"generated_at"`
Edges []fleetEdgeView `json:"edges"`
}
type fleetEdgeView struct {
EdgeID string `json:"edge_id"`
EdgeName string `json:"edge_name,omitempty"`
Version string `json:"version,omitempty"`
Protocol string `json:"protocol,omitempty"`
Connected bool `json:"connected"`
Health string `json:"health"`
LastSeen time.Time `json:"last_seen"`
NodeCount int `json:"node_count"`
Capabilities []edgeCapabilitySummaryView `json:"capabilities,omitempty"`
DomainAgents []edgeDomainAgentSummaryView `json:"domain_agents,omitempty"`
Error string `json:"error,omitempty"`
}
type fleetCommandResponse struct {
Operation string `json:"operation"`
Results []edgeCommandResponseView `json:"results"`
}
type edgeNodeSnapshotView struct {
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Label string `json:"label"`
Connected bool `json:"connected"`
Config *nodeConfigSummaryView `json:"config,omitempty"`
ProviderSnapshots []providerSnapshotView `json:"provider_snapshots,omitempty"`
}
type providerSnapshotView struct {
Adapter string `json:"adapter,omitempty"`
Status string `json:"status,omitempty"`
Capacity int32 `json:"capacity"`
InFlight int32 `json:"in_flight"`
Queued int32 `json:"queued"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Category string `json:"category,omitempty"`
ServedModels []string `json:"served_models,omitempty"`
Health string `json:"health,omitempty"`
LoadRatio float32 `json:"load_ratio"`
LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
}
type nodeConfigSummaryView struct {
Adapters []adapterSummaryView `json:"adapters"`
Concurrency int32 `json:"concurrency"`
}
type adapterSummaryView struct {
Type string `json:"type"`
Enabled bool `json:"enabled"`
}
// ── Transformation functions ─────────────────────────────────────────────────
func newNodeConfigSummaryView(payload *iop.NodeConfigPayload) *nodeConfigSummaryView {
if payload == nil {
return nil
}
var adapters []adapterSummaryView
for _, a := range payload.GetAdapters() {
adapters = append(adapters, adapterSummaryView{
Type: a.GetType(),
Enabled: a.GetEnabled(),
})
}
concurrency := int32(0)
if payload.GetRuntime() != nil {
concurrency = payload.GetRuntime().GetConcurrency()
}
return &nodeConfigSummaryView{
Adapters: adapters,
Concurrency: concurrency,
}
}
func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
if len(snaps) == 0 {
return nil
}
views := make([]providerSnapshotView, 0, len(snaps))
for _, p := range snaps {
views = append(views, providerSnapshotView{
Adapter: p.GetAdapter(),
Status: p.GetStatus(),
Capacity: p.GetCapacity(),
InFlight: p.GetInFlight(),
Queued: p.GetQueued(),
ID: p.GetId(),
Type: p.GetType(),
Category: p.GetCategory(),
ServedModels: append([]string(nil), p.GetServedModels()...),
Health: p.GetHealth(),
LoadRatio: p.GetLoadRatio(),
LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
})
}
return views
}
func edgeRegistryViewFromState(state wire.EdgeConnectionState) edgeRegistryView {
return edgeRegistryView{
EdgeID: state.EdgeID,
EdgeName: state.EdgeName,
Version: state.Version,
Capabilities: append([]string(nil), state.Capabilities...),
Protocol: state.Protocol,
Connected: state.Connected,
LastSeen: state.LastSeen,
}
}
func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusResponseView {
nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
for _, n := range resp.GetNodes() {
nodes = append(nodes, edgeNodeSnapshotView{
NodeID: n.GetNodeId(),
Alias: n.GetAlias(),
Label: n.GetLabel(),
Connected: n.GetConnected(),
Config: newNodeConfigSummaryView(n.GetConfig()),
ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots()),
})
}
return edgeStatusResponseView{
RequestID: resp.GetRequestId(),
EdgeID: resp.GetEdgeId(),
EdgeName: resp.GetEdgeName(),
ObservedTimeUnixNano: resp.GetObservedTimeUnixNano(),
Nodes: nodes,
Capabilities: edgeCapabilityViews(resp.GetCapabilities()),
DomainAgents: edgeDomainAgentViews(resp.GetDomainAgents()),
Metadata: resp.GetMetadata(),
Error: resp.GetError(),
}
}
func edgeCapabilityViews(caps []*iop.EdgeCapabilitySummary) []edgeCapabilitySummaryView {
if len(caps) == 0 {
return nil
}
views := make([]edgeCapabilitySummaryView, 0, len(caps))
for _, c := range caps {
views = append(views, edgeCapabilitySummaryView{
Kind: c.GetKind(),
Available: c.GetAvailable(),
Status: c.GetStatus(),
Summary: c.GetSummary(),
})
}
return views
}
func edgeDomainAgentViews(agents []*iop.EdgeDomainAgentSummary) []edgeDomainAgentSummaryView {
if len(agents) == 0 {
return nil
}
views := make([]edgeDomainAgentSummaryView, 0, len(agents))
for _, a := range agents {
views = append(views, edgeDomainAgentSummaryView{
AgentKind: a.GetAgentKind(),
Available: a.GetAvailable(),
LifecycleState: a.GetLifecycleState(),
ActiveCommandID: a.GetActiveCommandId(),
Summary: a.GetSummary(),
})
}
return views
}
func edgeCommandResponseViewFromProto(resp *iop.EdgeCommandResponse) edgeCommandResponseView {
return edgeCommandResponseView{
RequestID: resp.GetRequestId(),
CommandID: resp.GetCommandId(),
EdgeID: resp.GetEdgeId(),
Status: resp.GetStatus(),
Summary: resp.GetSummary(),
Error: resp.GetError(),
}
}
func edgeCommandRecordViewFromRecord(record wire.EdgeCommandRecord) edgeCommandRecordView {
view := edgeCommandRecordView{
EdgeID: record.EdgeID,
CommandID: record.CommandID,
Operation: record.Operation,
TargetSelector: record.TargetSelector,
Status: record.Status,
Summary: record.Summary,
Error: record.Error,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
}
if len(record.Events) > 0 {
view.Events = make([]edgeCommandEventView, 0, len(record.Events))
for _, e := range record.Events {
view.Events = append(view.Events, edgeCommandEventView{
Phase: e.Phase,
Summary: e.Summary,
OccurredAt: e.OccurredAt,
ReceivedAt: e.ReceivedAt,
})
}
}
return view
}
func edgeNodeEventViewFromRecord(record wire.EdgeNodeEventRecord) edgeNodeEventView {
event := record.Event
view := edgeNodeEventView{
EdgeID: record.EdgeID,
ReceivedAt: record.ReceivedAt,
}
if event == nil {
return view
}
if event.GetTimestamp() != 0 {
view.Timestamp = time.Unix(0, event.GetTimestamp()).UTC()
}
view.EventID = event.GetEventId()
view.Type = event.GetType()
view.Source = event.GetSource()
view.NodeID = event.GetNodeId()
view.Alias = event.GetAlias()
view.Reason = event.GetReason()
if metadata := event.GetMetadata(); len(metadata) > 0 {
view.Metadata = make(map[string]string, len(metadata))
for k, v := range metadata {
view.Metadata[k] = v
}
}
return view
}
func fleetEdgeViewFromState(state wire.EdgeConnectionState, requestStatus edgeStatusRequester, statusTimeout time.Duration) fleetEdgeView {
view := fleetEdgeView{
EdgeID: state.EdgeID,
EdgeName: state.EdgeName,
Version: state.Version,
Protocol: state.Protocol,
Connected: state.Connected,
LastSeen: state.LastSeen,
}
if !state.Connected {
view.Health = "offline"
return view
}
if requestStatus == nil {
view.Health = "online"
return view
}
resp, err := requestStatus(state.EdgeID, statusTimeout)
if err != nil {
view.Health = "degraded"
view.Error = err.Error()
return view
}
view.NodeCount = len(resp.GetNodes())
view.Capabilities = edgeCapabilityViews(resp.GetCapabilities())
view.DomainAgents = edgeDomainAgentViews(resp.GetDomainAgents())
if resp.GetError() != "" {
view.Health = "degraded"
view.Error = resp.GetError()
return view
}
view.Health = "online"
return view
}