Files
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

151 lines
5.6 KiB
Go

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
}