diff --git a/app/api_topology_test.go b/app/api_topology_test.go index 68752b98c..1c7eb66c4 100644 --- a/app/api_topology_test.go +++ b/app/api_topology_test.go @@ -136,7 +136,7 @@ func TestAPITopologyHosts(t *testing.T) { // Let's not unit-test the specific content of the detail tables } { - body := getRawJSON(t, ts, fmt.Sprintf("/api/topology/hosts/%s/%s", expected.ServerHostRenderedID, expected.ClientHostRenderedID)) + body := getRawJSON(t, ts, fmt.Sprintf("/api/topology/hosts/%s/%s", expected.ClientHostRenderedID, expected.ServerHostRenderedID)) var edge APIEdge if err := json.Unmarshal(body, &edge); err != nil { t.Fatalf("JSON parse error: %s", err) diff --git a/probe/endpoint/reporter.go b/probe/endpoint/reporter.go index 8912319f9..0c7c5bfad 100644 --- a/probe/endpoint/reporter.go +++ b/probe/endpoint/reporter.go @@ -77,13 +77,24 @@ func (r *Reporter) Report() (report.Report, error) { func (r *Reporter) addConnection(rpt *report.Report, c *procspy.Connection) { var ( + localIsClient = int(c.LocalPort) > int(c.RemotePort) localAddressNodeID = report.MakeAddressNodeID(r.hostID, c.LocalAddress.String()) remoteAddressNodeID = report.MakeAddressNodeID(r.hostID, c.RemoteAddress.String()) - adjecencyID = report.MakeAdjacencyID(localAddressNodeID) - edgeID = report.MakeEdgeID(localAddressNodeID, remoteAddressNodeID) + adjacencyID = "" + edgeID = "" ) - rpt.Address.Adjacency[adjecencyID] = rpt.Address.Adjacency[adjecencyID].Add(remoteAddressNodeID) + if localIsClient { + adjacencyID = report.MakeAdjacencyID(localAddressNodeID) + rpt.Address.Adjacency[adjacencyID] = rpt.Address.Adjacency[adjacencyID].Add(remoteAddressNodeID) + + edgeID = report.MakeEdgeID(localAddressNodeID, remoteAddressNodeID) + } else { + adjacencyID = report.MakeAdjacencyID(remoteAddressNodeID) + rpt.Address.Adjacency[adjacencyID] = rpt.Address.Adjacency[adjacencyID].Add(localAddressNodeID) + + edgeID = report.MakeEdgeID(remoteAddressNodeID, localAddressNodeID) + } if _, ok := rpt.Address.NodeMetadatas[localAddressNodeID]; !ok { rpt.Address.NodeMetadatas[localAddressNodeID] = report.MakeNodeMetadataWith(map[string]string{ @@ -98,11 +109,21 @@ func (r *Reporter) addConnection(rpt *report.Report, c *procspy.Connection) { var ( localEndpointNodeID = report.MakeEndpointNodeID(r.hostID, c.LocalAddress.String(), strconv.Itoa(int(c.LocalPort))) remoteEndpointNodeID = report.MakeEndpointNodeID(r.hostID, c.RemoteAddress.String(), strconv.Itoa(int(c.RemotePort))) - adjecencyID = report.MakeAdjacencyID(localEndpointNodeID) - edgeID = report.MakeEdgeID(localEndpointNodeID, remoteEndpointNodeID) + adjacencyID = "" + edgeID = "" ) - rpt.Endpoint.Adjacency[adjecencyID] = rpt.Endpoint.Adjacency[adjecencyID].Add(remoteEndpointNodeID) + if localIsClient { + adjacencyID = report.MakeAdjacencyID(localEndpointNodeID) + rpt.Endpoint.Adjacency[adjacencyID] = rpt.Endpoint.Adjacency[adjacencyID].Add(remoteEndpointNodeID) + + edgeID = report.MakeEdgeID(localEndpointNodeID, remoteEndpointNodeID) + } else { + adjacencyID = report.MakeAdjacencyID(remoteEndpointNodeID) + rpt.Endpoint.Adjacency[adjacencyID] = rpt.Endpoint.Adjacency[adjacencyID].Add(localEndpointNodeID) + + edgeID = report.MakeEdgeID(remoteEndpointNodeID, localEndpointNodeID) + } if _, ok := rpt.Endpoint.NodeMetadatas[localEndpointNodeID]; !ok { // First hit establishes NodeMetadata for scoped local address + port diff --git a/probe/endpoint/reporter_test.go b/probe/endpoint/reporter_test.go index 33a788ae5..455d18ada 100644 --- a/probe/endpoint/reporter_test.go +++ b/probe/endpoint/reporter_test.go @@ -85,14 +85,14 @@ func TestSpyNoProcesses(t *testing.T) { var ( scopedLocal = report.MakeAddressNodeID(nodeID, fixLocalAddress.String()) scopedRemote = report.MakeAddressNodeID(nodeID, fixRemoteAddress.String()) - localKey = report.MakeAdjacencyID(scopedLocal) + remoteKey = report.MakeAdjacencyID(scopedRemote) ) - if want, have := 1, len(r.Address.Adjacency[localKey]); want != have { + if want, have := 1, len(r.Address.Adjacency[remoteKey]); want != have { t.Fatalf("want %d, have %d", want, have) } - if want, have := scopedRemote, r.Address.Adjacency[localKey][0]; want != have { + if want, have := scopedLocal, r.Address.Adjacency[remoteKey][0]; want != have { t.Fatalf("want %q, have %q", want, have) } @@ -116,14 +116,14 @@ func TestSpyWithProcesses(t *testing.T) { var ( scopedLocal = report.MakeEndpointNodeID(nodeID, fixLocalAddress.String(), strconv.Itoa(int(fixLocalPort))) scopedRemote = report.MakeEndpointNodeID(nodeID, fixRemoteAddress.String(), strconv.Itoa(int(fixRemotePort))) - localKey = report.MakeAdjacencyID(scopedLocal) + remoteKey = report.MakeAdjacencyID(scopedRemote) ) - if want, have := 1, len(r.Endpoint.Adjacency[localKey]); want != have { + if want, have := 1, len(r.Endpoint.Adjacency[remoteKey]); want != have { t.Fatalf("want %d, have %d", want, have) } - if want, have := scopedRemote, r.Endpoint.Adjacency[localKey][0]; want != have { + if want, have := scopedLocal, r.Endpoint.Adjacency[remoteKey][0]; want != have { t.Fatalf("want %q, have %q", want, have) } diff --git a/render/detailed_node.go b/render/detailed_node.go index e03506f9d..2f329228a 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -48,6 +48,23 @@ type Row struct { ValueMinor string `json:"value_minor,omitempty"` // e.g. KB/s } +type rows []Row + +func (r rows) Len() int { return len(r) } +func (r rows) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r rows) Less(i, j int) bool { + switch { + case r[i].Key != r[j].Key: + return r[i].Key < r[j].Key + + case r[i].ValueMajor != r[j].ValueMajor: + return r[i].ValueMajor < r[j].ValueMajor + + default: + return r[i].ValueMinor < r[j].ValueMinor + } +} + type tables []Table func (t tables) Len() int { return len(t) } @@ -118,6 +135,7 @@ func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { } } if len(connections) > 0 { + sort.Sort(rows(connections)) tables = append(tables, connectionDetailsTable(connections)) } @@ -166,15 +184,38 @@ func connectionDetailsRows(topology report.Topology, originID string) []Row { if !ok { return rows } - adjacencies := topology.Adjacency[report.MakeAdjacencyID(originID)] - sort.Strings(adjacencies) - for _, nodeID := range adjacencies { - if remote, ok := labeler(nodeID); ok { - rows = append(rows, Row{ - Key: local, - ValueMajor: remote, - }) + // Firstly, collection outgoing connections from this node. + originAdjID := report.MakeAdjacencyID(originID) + for _, serverNodeID := range topology.Adjacency[originAdjID] { + remote, ok := labeler(serverNodeID) + if !ok { + continue } + rows = append(rows, Row{ + Key: local, + ValueMajor: remote, + }) + } + // Next, scan the topology for incoming connections to this node. + for clientAdjID, serverNodeIDs := range topology.Adjacency { + if clientAdjID == originAdjID { + continue + } + if !serverNodeIDs.Contains(originID) { + continue + } + clientNodeID, ok := report.ParseAdjacencyID(clientAdjID) + if !ok { + continue + } + remote, ok := labeler(clientNodeID) + if !ok { + continue + } + rows = append(rows, Row{ + Key: remote, + ValueMajor: local, + }) } return rows } @@ -183,7 +224,7 @@ func connectionDetailsTable(connectionRows []Row) Table { return Table{ Title: "Connection Details", Numeric: false, - Rows: append([]Row{{Key: "Local", ValueMajor: "Remote"}}, connectionRows...), + Rows: append([]Row{{Key: "Client", ValueMajor: "Server"}}, connectionRows...), Rank: endpointRank, } } diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go index 9219fee48..20b6f97cc 100644 --- a/render/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -94,8 +94,8 @@ func TestMakeDetailedHostNode(t *testing.T) { Rank: 0, Rows: []render.Row{ { - Key: "Local", - ValueMajor: "Remote", + Key: "Client", + ValueMajor: "Server", ValueMinor: "", }, { @@ -126,8 +126,8 @@ func TestMakeDetailedContainerNode(t *testing.T) { Numeric: true, Rank: 100, Rows: []render.Row{ - {"Egress packet rate", "75", "packets/sec"}, - {"Egress byte rate", "750", "Bps"}, + {"Egress packet rate", "105", "packets/sec"}, + {"Egress byte rate", "1.0", "KBps"}, }, }, { @@ -163,35 +163,35 @@ func TestMakeDetailedContainerNode(t *testing.T) { Title: "Connection Details", Numeric: false, Rows: []render.Row{ - {"Local", "Remote", ""}, + {"Client", "Server", ""}, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.UnknownClient1IP, test.ClientPort54010), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.UnknownClient1IP, test.ClientPort54020), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.UnknownClient3IP, test.ClientPort54020), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.ClientIP, test.ClientPort54001), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.ClientIP, test.ClientPort54002), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, { - fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), fmt.Sprintf("%s:%s", test.RandomClientIP, test.ClientPort12345), + fmt.Sprintf("%s:%s", test.ServerIP, test.ServerPort), "", }, }, diff --git a/render/expected/expected.go b/render/expected/expected.go index 8928bbdc0..194d99f2b 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -13,28 +13,45 @@ var ( uncontainedServerID = render.MakePseudoNodeID(render.UncontainedID, test.ServerHostName) unknownPseudoNode1ID = render.MakePseudoNodeID("10.10.10.10", test.ServerIP, "80") unknownPseudoNode2ID = render.MakePseudoNodeID("10.10.10.11", test.ServerIP, "80") - unknownPseudoNode1 = render.RenderableNode{ - ID: unknownPseudoNode1ID, - LabelMajor: "10.10.10.10", - Pseudo: true, - NodeMetadata: report.MakeNodeMetadata(), - EdgeMetadata: report.EdgeMetadata{}, + unknownPseudoNode1 = func(adjacency report.IDList) render.RenderableNode { + return render.RenderableNode{ + ID: unknownPseudoNode1ID, + LabelMajor: "10.10.10.10", + Pseudo: true, + NodeMetadata: report.MakeNodeMetadata(), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(70), + EgressByteCount: newu64(700), + }, + Adjacency: adjacency, + } } - unknownPseudoNode2 = render.RenderableNode{ - ID: unknownPseudoNode2ID, - LabelMajor: "10.10.10.11", - Pseudo: true, - NodeMetadata: report.MakeNodeMetadata(), - EdgeMetadata: report.EdgeMetadata{}, + unknownPseudoNode2 = func(adjacency report.IDList) render.RenderableNode { + return render.RenderableNode{ + ID: unknownPseudoNode2ID, + LabelMajor: "10.10.10.11", + Pseudo: true, + NodeMetadata: report.MakeNodeMetadata(), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(50), + EgressByteCount: newu64(500), + }, + Adjacency: adjacency, + } } - theInternetNode = render.RenderableNode{ - ID: render.TheInternetID, - LabelMajor: render.TheInternetMajor, - Pseudo: true, - NodeMetadata: report.MakeNodeMetadata(), - EdgeMetadata: report.EdgeMetadata{}, + theInternetNode = func(adjacency report.IDList) render.RenderableNode { + return render.RenderableNode{ + ID: render.TheInternetID, + LabelMajor: render.TheInternetMajor, + Pseudo: true, + NodeMetadata: report.MakeNodeMetadata(), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(60), + EgressByteCount: newu64(600), + }, + Adjacency: adjacency, + } } - ClientProcess1ID = render.MakeProcessID(test.ClientHostID, test.Client1PID) ClientProcess2ID = render.MakeProcessID(test.ClientHostID, test.Client2PID) ServerProcessID = render.MakeProcessID(test.ServerHostID, test.ServerPID) @@ -83,13 +100,7 @@ var ( LabelMinor: fmt.Sprintf("%s (%s)", test.ServerHostID, test.ServerPID), Rank: test.ServerComm, Pseudo: false, - Adjacency: report.MakeIDList( - ClientProcess1ID, - ClientProcess2ID, - unknownPseudoNode1ID, - unknownPseudoNode2ID, - render.TheInternetID, - ), + Adjacency: report.MakeIDList(), Origins: report.MakeIDList( test.Server80NodeID, test.ServerProcessNodeID, @@ -97,8 +108,8 @@ var ( ), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(150), - EgressByteCount: newu64(1500), + EgressPacketCount: newu64(210), + EgressByteCount: newu64(2100), }, }, nonContainerProcessID: { @@ -115,9 +126,9 @@ var ( NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, - unknownPseudoNode1ID: unknownPseudoNode1, - unknownPseudoNode2ID: unknownPseudoNode2, - render.TheInternetID: theInternetNode, + unknownPseudoNode1ID: unknownPseudoNode1(report.MakeIDList(ServerProcessID)), + unknownPseudoNode2ID: unknownPseudoNode2(report.MakeIDList(ServerProcessID)), + render.TheInternetID: theInternetNode(report.MakeIDList(ServerProcessID)), } RenderedProcessNames = render.RenderableNodes{ @@ -147,12 +158,7 @@ var ( LabelMinor: "", Rank: "apache", Pseudo: false, - Adjacency: report.MakeIDList( - "curl", - unknownPseudoNode1ID, - unknownPseudoNode2ID, - render.TheInternetID, - ), + Adjacency: report.MakeIDList(), Origins: report.MakeIDList( test.Server80NodeID, test.ServerProcessNodeID, @@ -160,8 +166,8 @@ var ( ), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(150), - EgressByteCount: newu64(1500), + EgressPacketCount: newu64(210), + EgressByteCount: newu64(2100), }, }, "bash": { @@ -177,9 +183,9 @@ var ( NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, - unknownPseudoNode1ID: unknownPseudoNode1, - unknownPseudoNode2ID: unknownPseudoNode2, - render.TheInternetID: theInternetNode, + unknownPseudoNode1ID: unknownPseudoNode1(report.MakeIDList("apache")), + unknownPseudoNode2ID: unknownPseudoNode2(report.MakeIDList("apache")), + render.TheInternetID: theInternetNode(report.MakeIDList("apache")), } RenderedContainers = render.RenderableNodes{ @@ -210,7 +216,7 @@ var ( LabelMinor: test.ServerHostName, Rank: test.ServerContainerImageID, Pseudo: false, - Adjacency: report.MakeIDList(test.ClientContainerID, render.TheInternetID), + Adjacency: report.MakeIDList(), Origins: report.MakeIDList( test.ServerContainerNodeID, test.Server80NodeID, @@ -219,8 +225,8 @@ var ( ), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(150), - EgressByteCount: newu64(1500), + EgressPacketCount: newu64(210), + EgressByteCount: newu64(2100), }, }, uncontainedServerID: { @@ -236,7 +242,7 @@ var ( NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, - render.TheInternetID: theInternetNode, + render.TheInternetID: theInternetNode(report.MakeIDList(test.ServerContainerID)), } RenderedContainerImages = render.RenderableNodes{ @@ -268,7 +274,7 @@ var ( LabelMinor: "", Rank: test.ServerContainerImageName, Pseudo: false, - Adjacency: report.MakeIDList(test.ClientContainerImageName, render.TheInternetID), + Adjacency: report.MakeIDList(), Origins: report.MakeIDList( test.ServerContainerImageNodeID, test.ServerContainerNodeID, @@ -277,8 +283,8 @@ var ( test.ServerHostNodeID), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(150), - EgressByteCount: newu64(1500), + EgressPacketCount: newu64(210), + EgressByteCount: newu64(2100), }, }, uncontainedServerID: { @@ -294,7 +300,7 @@ var ( NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, - render.TheInternetID: theInternetNode, + render.TheInternetID: theInternetNode(report.MakeIDList(test.ServerContainerImageName)), } ServerHostRenderedID = render.MakeHostID(test.ServerHostID) @@ -309,7 +315,7 @@ var ( LabelMinor: "hostname.com", // after first . Rank: "hostname.com", Pseudo: false, - Adjacency: report.MakeIDList(ClientHostRenderedID, render.TheInternetID, pseudoHostID1, pseudoHostID2), + Adjacency: report.MakeIDList(), Origins: report.MakeIDList( test.ServerHostNodeID, test.ServerAddressNodeID, @@ -339,6 +345,7 @@ var ( ID: pseudoHostID1, LabelMajor: "10.10.10.10", Pseudo: true, + Adjacency: report.MakeIDList(ServerHostRenderedID), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, @@ -346,10 +353,18 @@ var ( ID: pseudoHostID2, LabelMajor: "10.10.10.11", Pseudo: true, + Adjacency: report.MakeIDList(ServerHostRenderedID), + NodeMetadata: report.MakeNodeMetadata(), + EdgeMetadata: report.EdgeMetadata{}, + }, + render.TheInternetID: { + ID: render.TheInternetID, + LabelMajor: render.TheInternetMajor, + Pseudo: true, + Adjacency: report.MakeIDList(ServerHostRenderedID), NodeMetadata: report.MakeNodeMetadata(), EdgeMetadata: report.EdgeMetadata{}, }, - render.TheInternetID: theInternetNode, } ) diff --git a/render/mapping.go b/render/mapping.go index 66da21d6d..8d3369b25 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -33,11 +33,12 @@ const ( // rendered topology. type LeafMapFunc func(report.NodeMetadata) (RenderableNode, bool) -// PseudoFunc creates RenderableNode representing pseudo nodes given the dstNodeID. -// The srcNode renderable node is essentially from MapFunc, representing one of -// the rendered nodes this pseudo node refers to. srcNodeID and dstNodeID are -// node IDs prior to mapping. -type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string, local report.Networks) (RenderableNode, bool) +// PseudoFunc creates RenderableNode representing pseudo nodes given the +// srcNodeID. dstNodeID is the node id of one of the nodes this node has an +// edge to. srcNodeID and dstNodeID are node IDs prior to mapping. srcIsClient +// indicates the direction of the edge to dstNodeID - true indicates srcNodeID +// is the client, false indicates dstNodeID is the server. +type PseudoFunc func(srcNodeID, dstNodeID string, srcIsClient bool, local report.Networks) (RenderableNode, bool) // MapFunc is anything which can take an arbitrary RenderableNode and // return another RenderableNode. @@ -323,29 +324,36 @@ func MapAddress2Host(n RenderableNode) (RenderableNode, bool) { // the report's local networks. Otherwise, the returned function will // produce a single pseudo node per (dst address, src address, src port). func GenericPseudoNode(addresser func(id string) net.IP) PseudoFunc { - return func(src string, srcMapped RenderableNode, dst string, local report.Networks) (RenderableNode, bool) { - // Use the addresser to extract the destination IP - dstNodeAddr := addresser(dst) - + return func(srcNodeID, dstNodeID string, srcIsClient bool, local report.Networks) (RenderableNode, bool) { + // Use the addresser to extract the IP of the missing node + srcNodeAddr := addresser(srcNodeID) // If the dstNodeAddr is not in a network local to this report, we emit an // internet node - if !local.Contains(dstNodeAddr) { + if !local.Contains(srcNodeAddr) { return newPseudoNode(TheInternetID, TheInternetMajor, ""), true } - // Otherwise, the rule for non-internet psuedo nodes; emit 1 new node for each - // dstNodeAddr, srcNodeAddr, srcNodePort. - srcNodeAddr, srcNodePort := trySplitAddr(src) + if srcIsClient { + // If the client node is missing, generate a single pseudo node for every (client ip, server ip, server port) + serverIP, serverPort := trySplitAddr(dstNodeID) + outputID := MakePseudoNodeID(srcNodeAddr.String(), serverIP, serverPort) + major := srcNodeAddr.String() + return newPseudoNode(outputID, major, ""), true + } - outputID := MakePseudoNodeID(dstNodeAddr.String(), srcNodeAddr, srcNodePort) - major := dstNodeAddr.String() - return newPseudoNode(outputID, major, ""), true + // Otherwise (the server node is missing), generate a pseudo node for every (server ip, server port) + serverIP, serverPort := trySplitAddr(srcNodeID) + outputID := MakePseudoNodeID(serverIP, serverPort) + if serverPort != "" { + return newPseudoNode(outputID, serverIP+":"+serverPort, ""), true + } + return newPseudoNode(outputID, serverIP, ""), true } } // PanicPseudoNode just panics; it is for Topologies without edges -func PanicPseudoNode(src string, srcMapped RenderableNode, dst string, local report.Networks) (RenderableNode, bool) { - panic(dst) +func PanicPseudoNode(src, dst string, isClient bool, local report.Networks) (RenderableNode, bool) { + panic(src) } // trySplitAddr is basically ParseArbitraryNodeID, since its callsites diff --git a/render/render.go b/render/render.go index e80de1781..c6ea9c2f4 100644 --- a/render/render.go +++ b/render/render.go @@ -177,37 +177,69 @@ func (m LeafMap) Render(rpt report.Report) RenderableNodes { source2mapped[nodeID] = mapped.ID } + mkPseudoNode := func(srcNodeID, dstNodeID string, srcIsClient bool) (string, bool) { + pseudoNode, ok := m.Pseudo(srcNodeID, dstNodeID, srcIsClient, localNetworks) + if !ok { + return "", false + } + // TODO(tomwilkie): we should propagate origin nodes for pseudo nodes. + // Not worth doing until they are selectable in the UI + // pseudoNode.Origins = pseudoNode.Origins.Add(srcID) + existing, ok := nodes[pseudoNode.ID] + if ok { + pseudoNode.Merge(existing) + } + + nodes[pseudoNode.ID] = pseudoNode + source2mapped[pseudoNode.ID] = srcNodeID + return pseudoNode.ID, true + } + // Walk the graph and make connections. for src, dsts := range t.Adjacency { - var ( - srcNodeID, ok = report.ParseAdjacencyID(src) - srcRenderableID = source2mapped[srcNodeID] // must exist - srcRenderableNode = nodes[srcRenderableID] // must exist - ) + srcNodeID, ok := report.ParseAdjacencyID(src) if !ok { log.Printf("bad adjacency ID %q", src) continue } + srcRenderableID, ok := source2mapped[srcNodeID] + if !ok { + // One of the entries in dsts must be a non-pseudo node + var existingDstNodeID string + for _, dstNodeID := range dsts { + if _, ok := source2mapped[dstNodeID]; ok { + existingDstNodeID = dstNodeID + break + } + } + + srcRenderableID, ok = mkPseudoNode(srcNodeID, existingDstNodeID, true) + if !ok { + continue + } + } + srcRenderableNode := nodes[srcRenderableID] + for _, dstNodeID := range dsts { dstRenderableID, ok := source2mapped[dstNodeID] if !ok { - pseudoNode, ok := m.Pseudo(srcNodeID, srcRenderableNode, dstNodeID, localNetworks) + dstRenderableID, ok = mkPseudoNode(dstNodeID, srcNodeID, false) if !ok { continue } - dstRenderableID = pseudoNode.ID - nodes[dstRenderableID] = pseudoNode - source2mapped[dstNodeID] = dstRenderableID } + dstRenderableNode := nodes[dstRenderableID] srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID) - srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcNodeID) - edgeID := report.MakeEdgeID(srcNodeID, dstNodeID) - if md, ok := t.EdgeMetadatas[edgeID]; ok { - srcRenderableNode.EdgeMetadata.Merge(md) - } + // We propagate edge metadata to nodes on both ends of the edges. + // TODO we should 'reverse' one end of the edge meta data - ingress -> egress etc. + if md, ok := t.EdgeMetadatas[report.MakeEdgeID(srcNodeID, dstNodeID)]; ok { + srcRenderableNode.EdgeMetadata.Merge(md) + dstRenderableNode.EdgeMetadata.Merge(md) + nodes[dstRenderableID] = dstRenderableNode + } } nodes[srcRenderableID] = srcRenderableNode diff --git a/report/topology.go b/report/topology.go index 2ca9b3e0a..346805f60 100644 --- a/report/topology.go +++ b/report/topology.go @@ -85,8 +85,11 @@ func (t Topology) Validate() error { errs = append(errs, fmt.Sprintf("invalid edge ID %q", edgeID)) continue } + // For each edge, at least one of the ends must exist in nodemetadata if _, ok := t.NodeMetadatas[srcNodeID]; !ok { - errs = append(errs, fmt.Sprintf("node metadata missing for source node ID %q (from edge %q)", srcNodeID, edgeID)) + if _, ok := t.NodeMetadatas[dstNodeID]; !ok { + errs = append(errs, fmt.Sprintf("node metadatas missing for edge %q", edgeID)) + } } dstNodeIDs, ok := t.Adjacency[MakeAdjacencyID(srcNodeID)] if !ok { @@ -99,14 +102,19 @@ func (t Topology) Validate() error { } // Check all adjancency keys has entries in NodeMetadata. - for adjacencyID := range t.Adjacency { - nodeID, ok := ParseAdjacencyID(adjacencyID) + for adjacencyID, dsts := range t.Adjacency { + srcNodeID, ok := ParseAdjacencyID(adjacencyID) if !ok { errs = append(errs, fmt.Sprintf("invalid adjacency ID %q", adjacencyID)) continue } - if _, ok := t.NodeMetadatas[nodeID]; !ok { - errs = append(errs, fmt.Sprintf("node metadata missing for source node %q (from adjacency %q)", nodeID, adjacencyID)) + for _, dstNodeID := range dsts { + // For each edge, at least one of the ends must exist in nodemetadata + if _, ok := t.NodeMetadatas[srcNodeID]; !ok { + if _, ok := t.NodeMetadatas[dstNodeID]; !ok { + errs = append(errs, fmt.Sprintf("node metadata missing from adjacency %q -> %q", srcNodeID, dstNodeID)) + } + } } } diff --git a/test/report_fixture.go b/test/report_fixture.go index fe3d1db67..b84314239 100644 --- a/test/report_fixture.go +++ b/test/report_fixture.go @@ -79,11 +79,12 @@ var ( Report = report.Report{ Endpoint: report.Topology{ Adjacency: report.Adjacency{ - report.MakeAdjacencyID(Client54001NodeID): report.MakeIDList(Server80NodeID), - report.MakeAdjacencyID(Client54002NodeID): report.MakeIDList(Server80NodeID), - report.MakeAdjacencyID(Server80NodeID): report.MakeIDList( - Client54001NodeID, Client54002NodeID, UnknownClient1NodeID, UnknownClient2NodeID, - UnknownClient3NodeID, RandomClientNodeID), + report.MakeAdjacencyID(Client54001NodeID): report.MakeIDList(Server80NodeID), + report.MakeAdjacencyID(Client54002NodeID): report.MakeIDList(Server80NodeID), + report.MakeAdjacencyID(UnknownClient1NodeID): report.MakeIDList(Server80NodeID), + report.MakeAdjacencyID(UnknownClient2NodeID): report.MakeIDList(Server80NodeID), + report.MakeAdjacencyID(UnknownClient3NodeID): report.MakeIDList(Server80NodeID), + report.MakeAdjacencyID(RandomClientNodeID): report.MakeIDList(Server80NodeID), }, NodeMetadatas: report.NodeMetadatas{ // NodeMetadata is arbitrary. We're free to put only precisely what we @@ -117,27 +118,22 @@ var ( EgressPacketCount: newu64(20), EgressByteCount: newu64(200), }, - - report.MakeEdgeID(Server80NodeID, Client54001NodeID): report.EdgeMetadata{ - EgressPacketCount: newu64(10), - EgressByteCount: newu64(100), - }, - report.MakeEdgeID(Server80NodeID, Client54002NodeID): report.EdgeMetadata{ - EgressPacketCount: newu64(20), - EgressByteCount: newu64(200), - }, - report.MakeEdgeID(Server80NodeID, UnknownClient1NodeID): report.EdgeMetadata{ + report.MakeEdgeID(UnknownClient1NodeID, Server80NodeID): report.EdgeMetadata{ EgressPacketCount: newu64(30), EgressByteCount: newu64(300), }, - report.MakeEdgeID(Server80NodeID, UnknownClient2NodeID): report.EdgeMetadata{ + report.MakeEdgeID(UnknownClient2NodeID, Server80NodeID): report.EdgeMetadata{ EgressPacketCount: newu64(40), EgressByteCount: newu64(400), }, - report.MakeEdgeID(Server80NodeID, UnknownClient3NodeID): report.EdgeMetadata{ + report.MakeEdgeID(UnknownClient3NodeID, Server80NodeID): report.EdgeMetadata{ EgressPacketCount: newu64(50), EgressByteCount: newu64(500), }, + report.MakeEdgeID(RandomClientNodeID, Server80NodeID): report.EdgeMetadata{ + EgressPacketCount: newu64(60), + EgressByteCount: newu64(600), + }, }, }, Process: report.Topology{ @@ -201,9 +197,10 @@ var ( }, Address: report.Topology{ Adjacency: report.Adjacency{ - report.MakeAdjacencyID(ClientAddressNodeID): report.MakeIDList(ServerAddressNodeID), - report.MakeAdjacencyID(ServerAddressNodeID): report.MakeIDList( - ClientAddressNodeID, UnknownAddress1NodeID, UnknownAddress2NodeID, RandomAddressNodeID), // no backlinks to unknown/random + report.MakeAdjacencyID(ClientAddressNodeID): report.MakeIDList(ServerAddressNodeID), + report.MakeAdjacencyID(UnknownAddress1NodeID): report.MakeIDList(ServerAddressNodeID), + report.MakeAdjacencyID(UnknownAddress2NodeID): report.MakeIDList(ServerAddressNodeID), + report.MakeAdjacencyID(RandomAddressNodeID): report.MakeIDList(ServerAddressNodeID), }, NodeMetadatas: report.NodeMetadatas{ ClientAddressNodeID: report.MakeNodeMetadataWith(map[string]string{ @@ -219,9 +216,6 @@ var ( report.MakeEdgeID(ClientAddressNodeID, ServerAddressNodeID): report.EdgeMetadata{ MaxConnCountTCP: newu64(3), }, - report.MakeEdgeID(ServerAddressNodeID, ClientAddressNodeID): report.EdgeMetadata{ - MaxConnCountTCP: newu64(3), - }, }, }, Host: report.Topology{