- 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
386 lines
13 KiB
Go
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
|
|
}
|