This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/reverseproxy"
|
||||
)
|
||||
|
||||
type TopologyNode struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Known bool `json:"known"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
SourceID string `json:"sourceId,omitempty"`
|
||||
}
|
||||
|
||||
type TopologyEdge struct {
|
||||
ID string `json:"id"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
RelationType string `json:"relationType"`
|
||||
SourceID string `json:"sourceId,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Inferred bool `json:"inferred"`
|
||||
}
|
||||
|
||||
type Topology struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
ObservedAt time.Time `json:"observedAt"`
|
||||
CapabilityState string `json:"capabilityState"`
|
||||
ConfigurationState string `json:"configurationState"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Nodes []TopologyNode `json:"nodes"`
|
||||
Edges []TopologyEdge `json:"edges"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
TotalEdges int `json:"totalEdges"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
func BuildTopology(snapshot Snapshot, dependencies []Dependency, maxNodes, maxEdges int) (Topology, error) {
|
||||
return BuildTopologyWithRoutes(snapshot, dependencies, nil, maxNodes, maxEdges)
|
||||
}
|
||||
|
||||
func BuildTopologyWithRoutes(snapshot Snapshot, dependencies []Dependency, routes []reverseproxy.Route, maxNodes, maxEdges int) (Topology, error) {
|
||||
if maxNodes < 1 || maxNodes > 1000 || maxEdges < 1 || maxEdges > 2000 {
|
||||
return Topology{}, errors.New("topology bounds are outside safe limits")
|
||||
}
|
||||
if err := ValidateDependencyGraph(dependencies); err != nil {
|
||||
return Topology{}, err
|
||||
}
|
||||
services := append([]ServiceStatus(nil), snapshot.Services...)
|
||||
sort.SliceStable(services, func(i, j int) bool { return services[i].ID < services[j].ID })
|
||||
dependencies = append([]Dependency(nil), dependencies...)
|
||||
SortDependencies(dependencies)
|
||||
|
||||
nodes := make([]TopologyNode, 0, minInt(maxNodes, len(services)))
|
||||
byID := make(map[string]struct{}, maxNodes)
|
||||
for _, item := range services {
|
||||
if strings.TrimSpace(item.ID) == "" {
|
||||
continue
|
||||
}
|
||||
if len(nodes) >= maxNodes {
|
||||
break
|
||||
}
|
||||
nodes = append(nodes, TopologyNode{ID: item.ID, Label: item.Name, State: item.State, Reason: item.Reason, Known: true, Kind: "service", SourceID: item.SourceID})
|
||||
byID[item.ID] = struct{}{}
|
||||
}
|
||||
|
||||
truncated := len(services) > len(nodes)
|
||||
edges := make([]TopologyEdge, 0, minInt(maxEdges, len(dependencies)+len(routes)))
|
||||
for _, dependency := range dependencies {
|
||||
if len(edges) >= maxEdges {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
for _, id := range []string{dependency.ServiceID, dependency.DependsOnServiceID} {
|
||||
if _, exists := byID[id]; exists {
|
||||
continue
|
||||
}
|
||||
if len(nodes) >= maxNodes {
|
||||
truncated = true
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, TopologyNode{ID: id, Label: id, State: StateUnknown, Reason: "service_not_in_current_snapshot", Known: false, Kind: "service"})
|
||||
byID[id] = struct{}{}
|
||||
}
|
||||
if _, exists := byID[dependency.ServiceID]; !exists {
|
||||
truncated = true
|
||||
continue
|
||||
}
|
||||
if _, exists := byID[dependency.DependsOnServiceID]; !exists {
|
||||
truncated = true
|
||||
continue
|
||||
}
|
||||
edges = append(edges, TopologyEdge{ID: dependency.ID, From: dependency.ServiceID, To: dependency.DependsOnServiceID, RelationType: dependency.RelationType, SourceID: dependency.SourceID, Confidence: dependency.Confidence, Confirmed: dependency.Confirmed, Inferred: !dependency.Confirmed})
|
||||
}
|
||||
routeItems := append([]reverseproxy.Route(nil), routes...)
|
||||
sort.SliceStable(routeItems, func(i, j int) bool {
|
||||
if routeItems[i].ID != routeItems[j].ID {
|
||||
return routeItems[i].ID < routeItems[j].ID
|
||||
}
|
||||
return routeItems[i].Hostname < routeItems[j].Hostname
|
||||
})
|
||||
for _, route := range routeItems {
|
||||
if len(edges) >= maxEdges {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
routeNodeID := "reverse-proxy:" + route.ID
|
||||
if _, exists := byID[routeNodeID]; !exists {
|
||||
if len(nodes) >= maxNodes {
|
||||
truncated = true
|
||||
continue
|
||||
}
|
||||
reason := "reverse_proxy_route"
|
||||
if !route.Enabled {
|
||||
reason = "reverse_proxy_route_disabled"
|
||||
}
|
||||
nodes = append(nodes, TopologyNode{ID: routeNodeID, Label: route.URL, State: StateUnknown, Reason: reason, Known: true, Kind: "reverse_proxy", SourceID: route.SourceID})
|
||||
byID[routeNodeID] = struct{}{}
|
||||
}
|
||||
if strings.TrimSpace(route.TargetServiceID) == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := byID[route.TargetServiceID]; !exists {
|
||||
if len(nodes) >= maxNodes {
|
||||
truncated = true
|
||||
continue
|
||||
}
|
||||
nodes = append(nodes, TopologyNode{ID: route.TargetServiceID, Label: route.TargetServiceID, State: StateUnknown, Reason: "service_not_in_current_snapshot", Known: false, Kind: "service"})
|
||||
byID[route.TargetServiceID] = struct{}{}
|
||||
}
|
||||
edges = append(edges, TopologyEdge{ID: "reverse-proxy:" + route.ID, From: routeNodeID, To: route.TargetServiceID, RelationType: RelationExposes, SourceID: route.SourceID, Confidence: 1, Confirmed: route.Overridden, Inferred: !route.Overridden})
|
||||
}
|
||||
sort.SliceStable(nodes, func(i, j int) bool { return nodes[i].ID < nodes[j].ID })
|
||||
return Topology{ContractVersion: ContractVersion, ObservedAt: snapshot.ObservedAt.UTC(), CapabilityState: snapshot.CapabilityState, ConfigurationState: snapshot.ConfigurationState, Reason: snapshot.Reason, Nodes: nodes, Edges: edges, TotalNodes: len(nodes), TotalEdges: len(edges), Truncated: truncated}, nil
|
||||
}
|
||||
|
||||
func minInt(left, right int) int {
|
||||
if left < right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
Reference in New Issue
Block a user