diff --git a/app/router.go b/app/router.go index 04f2bc48d..4fa40f517 100644 --- a/app/router.go +++ b/app/router.go @@ -50,30 +50,30 @@ var topologyRegistry = map[string]topologyView{ "applications": { human: "Applications", parent: "", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: report.ProcessPID, Pseudo: report.GenericPseudoNode}, + renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessPID, Pseudo: render.GenericPseudoNode}, }, "applications-by-name": { human: "by name", parent: "applications", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: report.ProcessName, Pseudo: report.GenericGroupedPseudoNode}, + renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessName, Pseudo: render.GenericGroupedPseudoNode}, }, "containers": { human: "Containers", parent: "", renderer: render.Reduce([]render.Renderer{ - render.Map{Selector: report.SelectEndpoint, Mapper: report.MapEndpoint2Container, Pseudo: report.InternetOnlyPseudoNode}, - render.Map{Selector: report.SelectContainer, Mapper: report.MapContainerIdentity, Pseudo: report.InternetOnlyPseudoNode}, + render.Map{Selector: report.SelectEndpoint, Mapper: render.MapEndpoint2Container, Pseudo: render.InternetOnlyPseudoNode}, + render.Map{Selector: report.SelectContainer, Mapper: render.MapContainerIdentity, Pseudo: render.InternetOnlyPseudoNode}, }), }, "containers-by-image": { human: "by image", parent: "containers", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: report.ProcessContainerImage, Pseudo: report.InternetOnlyPseudoNode}, + renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessContainerImage, Pseudo: render.InternetOnlyPseudoNode}, }, "hosts": { human: "Hosts", parent: "", - renderer: render.Map{Selector: report.SelectAddress, Mapper: report.NetworkHostname, Pseudo: report.GenericPseudoNode}, + renderer: render.Map{Selector: report.SelectAddress, Mapper: render.NetworkHostname, Pseudo: render.GenericPseudoNode}, }, } diff --git a/experimental/graphviz/handle.go b/experimental/graphviz/handle.go index da48c6e92..382ec937f 100644 --- a/experimental/graphviz/handle.go +++ b/experimental/graphviz/handle.go @@ -8,13 +8,16 @@ import ( "sort" "strings" + "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) func handleTXT(r Reporter) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain") - dot(w, r.Report().Endpoint.RenderBy(mapFunc(req), nil)) + + renderer := render.Map{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} + dot(w, renderer.Render(r.Report())) //report.Render(r.Report(), report.SelectEndpoint, mapFunc(req), report.NoPseudoNode)) } @@ -32,7 +35,8 @@ func handleSVG(r Reporter) http.HandlerFunc { cmd.Stdout = w - dot(wc, r.Report().Endpoint.RenderBy(mapFunc(req), nil)) + renderer := render.Map{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} + dot(wc, renderer.Render(r.Report())) wc.Close() w.Header().Set("Content-Type", "image/svg+xml") @@ -98,12 +102,12 @@ func engine(r *http.Request) string { return engine } -func mapFunc(r *http.Request) report.MapFunc { +func mapFunc(r *http.Request) render.MapFunc { switch strings.ToLower(r.FormValue("map_func")) { case "hosts", "networkhost", "networkhostname": - return report.NetworkHostname + return render.NetworkHostname } - return report.ProcessPID + return render.ProcessPID } func classView(r *http.Request) bool { diff --git a/report/mapping.go b/render/mapping.go similarity index 73% rename from report/mapping.go rename to render/mapping.go index fc5d7ef6b..9771b0eb6 100644 --- a/report/mapping.go +++ b/render/mapping.go @@ -1,31 +1,33 @@ -package report +package render import ( "fmt" "strings" + + "github.com/weaveworks/scope/report" ) const humanTheInternet = "the Internet" -func newRenderableNode(id, major, minor, rank string) RenderableNode { - return RenderableNode{ +func newRenderableNode(id, major, minor, rank string) report.RenderableNode { + return report.RenderableNode{ ID: id, LabelMajor: major, LabelMinor: minor, Rank: rank, Pseudo: false, - Metadata: AggregateMetadata{}, + Metadata: report.AggregateMetadata{}, } } -func newPseudoNode(id, major, minor string) RenderableNode { - return RenderableNode{ +func newPseudoNode(id, major, minor string) report.RenderableNode { + return report.RenderableNode{ ID: id, LabelMajor: major, LabelMinor: minor, Rank: "", Pseudo: true, - Metadata: AggregateMetadata{}, + Metadata: report.AggregateMetadata{}, } } @@ -39,36 +41,18 @@ func newPseudoNode(id, major, minor string) RenderableNode { // // If the final output parameter is false, the node shall be omitted from the // rendered topology. -type MapFunc func(NodeMetadata) (RenderableNode, bool) +type MapFunc func(report.NodeMetadata) (report.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) (RenderableNode, bool) - -// TopologySelector selects a single topology from a report. -type TopologySelector func(r Report) Topology - -// SelectEndpoint selects the endpoint topology. -func SelectEndpoint(r Report) Topology { - return r.Endpoint -} - -// SelectAddress selects the address topology. -func SelectAddress(r Report) Topology { - return r.Address -} - -// SelectContainer selects the container topology. -func SelectContainer(r Report) Topology { - return r.Container -} +type PseudoFunc func(srcNodeID string, srcNode report.RenderableNode, dstNodeID string) (report.RenderableNode, bool) // ProcessPID takes a node NodeMetadata from topology, and returns a // representation with the ID based on the process PID and the labels based on // the process name. -func ProcessPID(m NodeMetadata) (RenderableNode, bool) { +func ProcessPID(m report.NodeMetadata) (report.RenderableNode, bool) { var ( identifier = fmt.Sprintf("%s:%s:%s", "pid", m["domain"], m["pid"]) minor = fmt.Sprintf("%s (%s)", m["domain"], m["pid"]) @@ -81,7 +65,7 @@ func ProcessPID(m NodeMetadata) (RenderableNode, bool) { // ProcessName takes a node NodeMetadata from a topology, and returns a // representation with the ID based on the process name (grouping all // processes with the same name together). -func ProcessName(m NodeMetadata) (RenderableNode, bool) { +func ProcessName(m report.NodeMetadata) (report.RenderableNode, bool) { show := m["pid"] != "" && m["name"] != "" return newRenderableNode(m["name"], m["name"], "", m["name"]), show } @@ -90,7 +74,7 @@ func ProcessName(m NodeMetadata) (RenderableNode, bool) { // in. We consider container and image IDs to be globally unique, and so don't // scope them further by e.g. host. If no container metadata is found, nodes are // grouped into the Uncontained node. -func MapEndpoint2Container(m NodeMetadata) (RenderableNode, bool) { +func MapEndpoint2Container(m report.NodeMetadata) (report.RenderableNode, bool) { var id, major, minor, rank string if m["docker_container_id"] == "" { id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained" @@ -102,7 +86,7 @@ func MapEndpoint2Container(m NodeMetadata) (RenderableNode, bool) { } // MapContainerIdentity maps container topology node to container mapped nodes. -func MapContainerIdentity(m NodeMetadata) (RenderableNode, bool) { +func MapContainerIdentity(m report.NodeMetadata) (report.RenderableNode, bool) { var id, major, minor, rank string if m["docker_container_id"] == "" { id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained" @@ -116,7 +100,7 @@ func MapContainerIdentity(m NodeMetadata) (RenderableNode, bool) { // ProcessContainerImage maps topology nodes to the container images they run // on. If no container metadata is found, nodes are grouped into the // Uncontained node. -func ProcessContainerImage(m NodeMetadata) (RenderableNode, bool) { +func ProcessContainerImage(m report.NodeMetadata) (report.RenderableNode, bool) { var id, major, minor, rank string if m["docker_image_id"] == "" { id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained" @@ -130,7 +114,7 @@ func ProcessContainerImage(m NodeMetadata) (RenderableNode, bool) { // NetworkHostname takes a node NodeMetadata and returns a representation // based on the hostname. Major label is the hostname, the minor label is the // domain, if any. -func NetworkHostname(m NodeMetadata) (RenderableNode, bool) { +func NetworkHostname(m report.NodeMetadata) (report.RenderableNode, bool) { var ( name = m["name"] domain = "" @@ -146,10 +130,10 @@ func NetworkHostname(m NodeMetadata) (RenderableNode, bool) { // GenericPseudoNode contains heuristics for building sensible pseudo nodes. // It should go away. -func GenericPseudoNode(src string, srcMapped RenderableNode, dst string) (RenderableNode, bool) { +func GenericPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) { var maj, min, outputID string - if dst == TheInternet { + if dst == report.TheInternet { outputID = dst maj, min = humanTheInternet, "" } else { @@ -158,7 +142,7 @@ func GenericPseudoNode(src string, srcMapped RenderableNode, dst string) (Render srcNodeAddr, srcNodePort := trySplitAddr(src) dstNodeAddr, _ := trySplitAddr(dst) - outputID = MakePseudoNodeID(dstNodeAddr, srcNodeAddr, srcNodePort) + outputID = report.MakePseudoNodeID(dstNodeAddr, srcNodeAddr, srcNodePort) maj, min = dstNodeAddr, "" } @@ -167,17 +151,17 @@ func GenericPseudoNode(src string, srcMapped RenderableNode, dst string) (Render // GenericGroupedPseudoNode contains heuristics for building sensible pseudo nodes. // It should go away. -func GenericGroupedPseudoNode(src string, srcMapped RenderableNode, dst string) (RenderableNode, bool) { +func GenericGroupedPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) { var maj, min, outputID string - if dst == TheInternet { + if dst == report.TheInternet { outputID = dst maj, min = humanTheInternet, "" } else { // When grouping, emit one pseudo node per (srcNodeAddress, dstNodeAddr) dstNodeAddr, _ := trySplitAddr(dst) - outputID = MakePseudoNodeID(dstNodeAddr, srcMapped.ID) + outputID = report.MakePseudoNodeID(dstNodeAddr, srcMapped.ID) maj, min = dstNodeAddr, "" } @@ -185,11 +169,11 @@ func GenericGroupedPseudoNode(src string, srcMapped RenderableNode, dst string) } // InternetOnlyPseudoNode never creates a pseudo node, unless it's the Internet. -func InternetOnlyPseudoNode(_ string, _ RenderableNode, dst string) (RenderableNode, bool) { - if dst == TheInternet { - return newPseudoNode(TheInternet, humanTheInternet, ""), true +func InternetOnlyPseudoNode(_ string, _ report.RenderableNode, dst string) (report.RenderableNode, bool) { + if dst == report.TheInternet { + return newPseudoNode(report.TheInternet, humanTheInternet, ""), true } - return RenderableNode{}, false + return report.RenderableNode{}, false } // trySplitAddr is basically ParseArbitraryNodeID, since its callsites @@ -199,7 +183,7 @@ func InternetOnlyPseudoNode(_ string, _ RenderableNode, dst string) (RenderableN // // TODO change how pseudofuncs work, and eliminate this helper. func trySplitAddr(addr string) (string, string) { - fields := strings.SplitN(addr, ScopeDelim, 3) + fields := strings.SplitN(addr, report.ScopeDelim, 3) if len(fields) == 3 { return fields[1], fields[2] } diff --git a/report/mapping_test.go b/render/mapping_test.go similarity index 87% rename from report/mapping_test.go rename to render/mapping_test.go index 022d93b64..fa2e3470c 100644 --- a/report/mapping_test.go +++ b/render/mapping_test.go @@ -1,22 +1,24 @@ -package report +package render import ( "fmt" "testing" + + "github.com/weaveworks/scope/report" ) func TestUngroupedMapping(t *testing.T) { for i, c := range []struct { f MapFunc id string - meta NodeMetadata + meta report.NodeMetadata wantOK bool wantID, wantMajor, wantMinor, wantRank string }{ { f: NetworkHostname, - id: MakeAddressNodeID("", "1.2.3.4"), - meta: NodeMetadata{ + id: report.MakeAddressNodeID("", "1.2.3.4"), + meta: report.NodeMetadata{ "name": "my.host", }, wantOK: true, @@ -27,8 +29,8 @@ func TestUngroupedMapping(t *testing.T) { }, { f: NetworkHostname, - id: MakeAddressNodeID("", "1.2.3.4"), - meta: NodeMetadata{ + id: report.MakeAddressNodeID("", "1.2.3.4"), + meta: report.NodeMetadata{ "name": "localhost", }, wantOK: true, @@ -40,7 +42,7 @@ func TestUngroupedMapping(t *testing.T) { { f: ProcessPID, id: "not-used-beta", - meta: NodeMetadata{ + meta: report.NodeMetadata{ "pid": "42", "name": "curl", "domain": "hosta", @@ -54,7 +56,7 @@ func TestUngroupedMapping(t *testing.T) { { f: MapEndpoint2Container, id: "foo-id", - meta: NodeMetadata{ + meta: report.NodeMetadata{ "pid": "42", "name": "curl", "domain": "hosta", @@ -68,7 +70,7 @@ func TestUngroupedMapping(t *testing.T) { { f: MapEndpoint2Container, id: "bar-id", - meta: NodeMetadata{ + meta: report.NodeMetadata{ "pid": "42", "name": "curl", "domain": "hosta", diff --git a/render/render.go b/render/render.go index d2f03890f..a59b0bc81 100644 --- a/render/render.go +++ b/render/render.go @@ -1,6 +1,8 @@ package render import ( + "log" + "github.com/weaveworks/scope/report" ) @@ -36,16 +38,120 @@ func (r Reduce) AggregateMetadata(rpt report.Report, localID, remoteID string) r // Mapper functions and topology selector. type Map struct { Selector report.TopologySelector - Mapper report.MapFunc - Pseudo report.PseudoFunc + Mapper MapFunc + Pseudo PseudoFunc } // Render produces a set of RenderableNodes given a Report func (m Map) Render(rpt report.Report) report.RenderableNodes { - return m.Selector(rpt).RenderBy(m.Mapper, m.Pseudo) + return renderTopology(m.Selector(rpt), m.Mapper, m.Pseudo) +} + +// RenderBy transforms a given Topology into a set of RenderableNodes, which +// the UI will render collectively as a graph. Note that a RenderableNode will +// always be rendered with other nodes, and therefore contains limited detail. +// +// RenderBy takes a a MapFunc, which defines how to group and label nodes. Npdes +// with the same mapped IDs will be merged. +func renderTopology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) report.RenderableNodes { + nodes := report.RenderableNodes{} + + // Build a set of RenderableNodes for all non-pseudo probes, and an + // addressID to nodeID lookup map. Multiple addressIDs can map to the same + // RenderableNodes. + var ( + source2mapped = map[string]string{} // source node ID -> mapped node ID + source2host = map[string]string{} // source node ID -> origin host ID + ) + for nodeID, metadata := range t.NodeMetadatas { + mapped, ok := mapFunc(metadata) + if !ok { + continue + } + + // mapped.ID needs not be unique over all addressIDs. If not, we merge with + // the existing data, on the assumption that the MapFunc returns the same + // data. + existing, ok := nodes[mapped.ID] + if ok { + mapped.Merge(existing) + } + + mapped.Origins = mapped.Origins.Add(nodeID) + nodes[mapped.ID] = mapped + source2mapped[nodeID] = mapped.ID + source2host[nodeID] = metadata[report.HostNodeID] + } + + // Walk the graph and make connections. + for src, dsts := range t.Adjacency { + var ( + srcNodeID, ok = report.ParseAdjacencyID(src) + //srcOriginHostID, _, ok2 = ParseNodeID(srcNodeID) + srcHostNodeID = source2host[srcNodeID] + srcRenderableID = source2mapped[srcNodeID] // must exist + srcRenderableNode = nodes[srcRenderableID] // must exist + ) + if !ok { + log.Printf("bad adjacency ID %q", src) + continue + } + + for _, dstNodeID := range dsts { + dstRenderableID, ok := source2mapped[dstNodeID] + if !ok { + pseudoNode, ok := pseudoFunc(srcNodeID, srcRenderableNode, dstNodeID) + if !ok { + continue + } + dstRenderableID = pseudoNode.ID + nodes[dstRenderableID] = pseudoNode + source2mapped[dstNodeID] = dstRenderableID + } + + srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID) + srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcHostNodeID) + srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcNodeID) + edgeID := report.MakeEdgeID(srcNodeID, dstNodeID) + if md, ok := t.EdgeMetadatas[edgeID]; ok { + srcRenderableNode.Metadata.Merge(md.Transform()) + } + } + + nodes[srcRenderableID] = srcRenderableNode + } + + return nodes } // AggregateMetadata produces an AggregateMetadata for a given edge func (m Map) AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata { - return m.Selector(rpt).EdgeMetadata(m.Mapper, localID, remoteID).Transform() + return edgeMetadata(m.Selector(rpt), m.Mapper, localID, remoteID).Transform() +} + +// EdgeMetadata gives the metadata of an edge from the perspective of the +// srcRenderableID. Since an edgeID can have multiple edges on the address +// level, it uses the supplied mapping function to translate address IDs to +// renderable node (mapped) IDs. +func edgeMetadata(t report.Topology, mapFunc MapFunc, srcRenderableID, dstRenderableID string) report.EdgeMetadata { + metadata := report.EdgeMetadata{} + for edgeID, edgeMeta := range t.EdgeMetadatas { + src, dst, ok := report.ParseEdgeID(edgeID) + if !ok { + log.Printf("bad edge ID %q", edgeID) + continue + } + if src != report.TheInternet { + mapped, _ := mapFunc(t.NodeMetadatas[src]) + src = mapped.ID + } + if dst != report.TheInternet { + mapped, _ := mapFunc(t.NodeMetadatas[dst]) + dst = mapped.ID + } + if src == srcRenderableID && dst == dstRenderableID { + metadata.Flatten(edgeMeta) + } + } + return metadata } diff --git a/render/render_test.go b/render/render_test.go index 87e98e933..a7a885b0b 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -1,13 +1,19 @@ -package render_test +package render import ( "reflect" "testing" - "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" ) +func init() { + spew.Config.SortKeys = true // :\ +} + type mockRenderer struct { report.RenderableNodes aggregateMetadata report.AggregateMetadata @@ -21,7 +27,7 @@ func (m mockRenderer) AggregateMetadata(rpt report.Report, localID, remoteID str } func TestReduceRender(t *testing.T) { - renderer := render.Reduce([]render.Renderer{ + renderer := Reduce([]Renderer{ mockRenderer{RenderableNodes: report.RenderableNodes{"foo": {ID: "foo"}}}, mockRenderer{RenderableNodes: report.RenderableNodes{"bar": {ID: "bar"}}}, }) @@ -35,7 +41,7 @@ func TestReduceRender(t *testing.T) { } func TestReduceEdge(t *testing.T) { - renderer := render.Reduce([]render.Renderer{ + renderer := Reduce([]Renderer{ mockRenderer{aggregateMetadata: report.AggregateMetadata{"foo": 1}}, mockRenderer{aggregateMetadata: report.AggregateMetadata{"bar": 2}}, }) @@ -47,3 +53,323 @@ func TestReduceEdge(t *testing.T) { t.Errorf("want %+v, have %+v", want, have) } } + +var ( + clientHostID = "client.hostname.com" + serverHostID = "server.hostname.com" + randomHostID = "random.hostname.com" + unknownHostID = "" + + clientHostNodeID = report.MakeHostNodeID(clientHostID) + serverHostNodeID = report.MakeHostNodeID(serverHostID) + randomHostNodeID = report.MakeHostNodeID(randomHostID) + + client54001 = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54001") // curl (1) + client54002 = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54002") // curl (2) + unknownClient1 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54010") // we want to ensure two unknown clients, connnected + unknownClient2 = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54020") // to the same server, are deduped. + unknownClient3 = report.MakeEndpointNodeID(serverHostID, "10.10.10.11", "54020") // Check this one isn't deduped + server80 = report.MakeEndpointNodeID(serverHostID, "192.168.1.1", "80") // apache + + clientIP = report.MakeAddressNodeID(clientHostID, "10.10.10.20") + serverIP = report.MakeAddressNodeID(serverHostID, "192.168.1.1") + randomIP = report.MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology + unknownIP = report.MakeAddressNodeID(unknownHostID, "10.10.10.10") +) + +var ( + rpt = report.Report{ + Endpoint: report.Topology{ + Adjacency: report.Adjacency{ + report.MakeAdjacencyID(client54001): report.MakeIDList(server80), + report.MakeAdjacencyID(client54002): report.MakeIDList(server80), + report.MakeAdjacencyID(server80): report.MakeIDList(client54001, client54002, unknownClient1, unknownClient2, unknownClient3), + }, + NodeMetadatas: report.NodeMetadatas{ + // NodeMetadata is arbitrary. We're free to put only precisely what we + // care to test into the fixture. Just be sure to include the bits + // that the mapping funcs extract :) + client54001: report.NodeMetadata{ + "name": "curl", + "domain": "client-54001-domain", + "pid": "10001", + report.HostNodeID: clientHostNodeID, + }, + client54002: report.NodeMetadata{ + "name": "curl", // should be same as above! + "domain": "client-54002-domain", // may be different than above + "pid": "10001", // should be same as above! + report.HostNodeID: clientHostNodeID, + }, + server80: report.NodeMetadata{ + "name": "apache", + "domain": "server-80-domain", + "pid": "215", + report.HostNodeID: serverHostNodeID, + }, + }, + EdgeMetadatas: report.EdgeMetadatas{ + report.MakeEdgeID(client54001, server80): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 100, + BytesEgress: 10, + }, + report.MakeEdgeID(client54002, server80): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 200, + BytesEgress: 20, + }, + + report.MakeEdgeID(server80, client54001): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 10, + BytesEgress: 100, + }, + report.MakeEdgeID(server80, client54002): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 20, + BytesEgress: 200, + }, + report.MakeEdgeID(server80, unknownClient1): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 30, + BytesEgress: 300, + }, + report.MakeEdgeID(server80, unknownClient2): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 40, + BytesEgress: 400, + }, + report.MakeEdgeID(server80, unknownClient3): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 50, + BytesEgress: 500, + }, + }, + }, + Address: report.Topology{ + Adjacency: report.Adjacency{ + report.MakeAdjacencyID(clientIP): report.MakeIDList(serverIP), + report.MakeAdjacencyID(randomIP): report.MakeIDList(serverIP), + report.MakeAdjacencyID(serverIP): report.MakeIDList(clientIP, unknownIP), // no backlink to random + }, + NodeMetadatas: report.NodeMetadatas{ + clientIP: report.NodeMetadata{ + "name": "client.hostname.com", // hostname + report.HostNodeID: clientHostNodeID, + }, + randomIP: report.NodeMetadata{ + "name": "random.hostname.com", // hostname + report.HostNodeID: randomHostNodeID, + }, + serverIP: report.NodeMetadata{ + "name": "server.hostname.com", // hostname + report.HostNodeID: serverHostNodeID, + }, + }, + EdgeMetadatas: report.EdgeMetadatas{ + report.MakeEdgeID(clientIP, serverIP): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 3, + }, + report.MakeEdgeID(randomIP, serverIP): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 20, // dangling connections, weird but possible + }, + report.MakeEdgeID(serverIP, clientIP): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 3, + }, + report.MakeEdgeID(serverIP, unknownIP): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 7, + }, + }, + }, + } +) + +func TestRenderByEndpointPID(t *testing.T) { + want := report.RenderableNodes{ + "pid:client-54001-domain:10001": { + ID: "pid:client-54001-domain:10001", + LabelMajor: "curl", + LabelMinor: "client-54001-domain (10001)", + Rank: "10001", + Pseudo: false, + Adjacency: report.MakeIDList("pid:server-80-domain:215"), + Origins: report.MakeIDList(report.MakeHostNodeID("client.hostname.com"), report.MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54001")), + Metadata: report.AggregateMetadata{ + report.KeyBytesIngress: 100, + report.KeyBytesEgress: 10, + }, + }, + "pid:client-54002-domain:10001": { + ID: "pid:client-54002-domain:10001", + LabelMajor: "curl", + LabelMinor: "client-54002-domain (10001)", + Rank: "10001", // same process + Pseudo: false, + Adjacency: report.MakeIDList("pid:server-80-domain:215"), + Origins: report.MakeIDList(report.MakeHostNodeID("client.hostname.com"), report.MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54002")), + Metadata: report.AggregateMetadata{ + report.KeyBytesIngress: 200, + report.KeyBytesEgress: 20, + }, + }, + "pid:server-80-domain:215": { + ID: "pid:server-80-domain:215", + LabelMajor: "apache", + LabelMinor: "server-80-domain (215)", + Rank: "215", + Pseudo: false, + Adjacency: report.MakeIDList( + "pid:client-54001-domain:10001", + "pid:client-54002-domain:10001", + "pseudo;10.10.10.10;192.168.1.1;80", + "pseudo;10.10.10.11;192.168.1.1;80", + ), + Origins: report.MakeIDList(report.MakeHostNodeID("server.hostname.com"), report.MakeEndpointNodeID("server.hostname.com", "192.168.1.1", "80")), + Metadata: report.AggregateMetadata{ + report.KeyBytesIngress: 150, + report.KeyBytesEgress: 1500, + }, + }, + "pseudo;10.10.10.10;192.168.1.1;80": { + ID: "pseudo;10.10.10.10;192.168.1.1;80", + LabelMajor: "10.10.10.10", + Pseudo: true, + Metadata: report.AggregateMetadata{}, + }, + "pseudo;10.10.10.11;192.168.1.1;80": { + ID: "pseudo;10.10.10.11;192.168.1.1;80", + LabelMajor: "10.10.10.11", + Pseudo: true, + Metadata: report.AggregateMetadata{}, + }, + } + have := renderTopology(rpt.Endpoint, ProcessPID, GenericPseudoNode) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func TestRenderByEndpointPIDGrouped(t *testing.T) { + // For grouped, I've somewhat arbitrarily chosen to squash together all + // processes with the same name by removing the PID and domain (host) + // dimensions from the ID. That could be changed. + want := report.RenderableNodes{ + "curl": { + ID: "curl", + LabelMajor: "curl", + LabelMinor: "", + Rank: "curl", + Pseudo: false, + Adjacency: report.MakeIDList("apache"), + Origins: report.MakeIDList(report.MakeHostNodeID("client.hostname.com"), report.MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54001"), report.MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54002")), + Metadata: report.AggregateMetadata{ + report.KeyBytesIngress: 300, + report.KeyBytesEgress: 30, + }, + }, + "apache": { + ID: "apache", + LabelMajor: "apache", + LabelMinor: "", + Rank: "apache", + Pseudo: false, + Adjacency: report.MakeIDList( + "curl", + "pseudo;10.10.10.10;apache", + "pseudo;10.10.10.11;apache", + ), + Origins: report.MakeIDList(report.MakeHostNodeID("server.hostname.com"), report.MakeEndpointNodeID("server.hostname.com", "192.168.1.1", "80")), + Metadata: report.AggregateMetadata{ + report.KeyBytesIngress: 150, + report.KeyBytesEgress: 1500, + }, + }, + "pseudo;10.10.10.10;apache": { + ID: "pseudo;10.10.10.10;apache", + LabelMajor: "10.10.10.10", + Pseudo: true, + Metadata: report.AggregateMetadata{}, + }, + "pseudo;10.10.10.11;apache": { + ID: "pseudo;10.10.10.11;apache", + LabelMajor: "10.10.10.11", + Pseudo: true, + Metadata: report.AggregateMetadata{}, + }, + } + have := renderTopology(rpt.Endpoint, ProcessName, GenericGroupedPseudoNode) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func TestRenderByNetworkHostname(t *testing.T) { + want := report.RenderableNodes{ + "host:client.hostname.com": { + ID: "host:client.hostname.com", + LabelMajor: "client", // before first . + LabelMinor: "hostname.com", // after first . + Rank: "client", + Pseudo: false, + Adjacency: report.MakeIDList("host:server.hostname.com"), + Origins: report.MakeIDList(report.MakeHostNodeID("client.hostname.com"), report.MakeAddressNodeID("client.hostname.com", "10.10.10.20")), + Metadata: report.AggregateMetadata{ + report.KeyMaxConnCountTCP: 3, + }, + }, + "host:random.hostname.com": { + ID: "host:random.hostname.com", + LabelMajor: "random", // before first . + LabelMinor: "hostname.com", // after first . + Rank: "random", + Pseudo: false, + Adjacency: report.MakeIDList("host:server.hostname.com"), + Origins: report.MakeIDList(report.MakeHostNodeID("random.hostname.com"), report.MakeAddressNodeID("random.hostname.com", "172.16.11.9")), + Metadata: report.AggregateMetadata{ + report.KeyMaxConnCountTCP: 20, + }, + }, + "host:server.hostname.com": { + ID: "host:server.hostname.com", + LabelMajor: "server", // before first . + LabelMinor: "hostname.com", // after first . + Rank: "server", + Pseudo: false, + Adjacency: report.MakeIDList("host:client.hostname.com", "pseudo;10.10.10.10;192.168.1.1;"), + Origins: report.MakeIDList(report.MakeHostNodeID("server.hostname.com"), report.MakeAddressNodeID("server.hostname.com", "192.168.1.1")), + Metadata: report.AggregateMetadata{ + report.KeyMaxConnCountTCP: 10, + }, + }, + "pseudo;10.10.10.10;192.168.1.1;": { + ID: "pseudo;10.10.10.10;192.168.1.1;", + LabelMajor: "10.10.10.10", + LabelMinor: "", // after first . + Rank: "", + Pseudo: true, + Adjacency: nil, + Origins: nil, + Metadata: report.AggregateMetadata{}, + }, + } + have := renderTopology(rpt.Address, NetworkHostname, GenericPseudoNode) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func diff(want, have interface{}) string { + text, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(spew.Sdump(want)), + B: difflib.SplitLines(spew.Sdump(have)), + FromFile: "want", + ToFile: "have", + Context: 3, + }) + return "\n" + text +} diff --git a/report/report.go b/report/report.go index 3a6fdad1c..1e43deddb 100644 --- a/report/report.go +++ b/report/report.go @@ -82,6 +82,24 @@ type Row struct { ValueMinor string `json:"value_minor,omitempty"` // e.g. KB/s } +// TopologySelector selects a single topology from a report. +type TopologySelector func(r Report) Topology + +// SelectEndpoint selects the endpoint topology. +func SelectEndpoint(r Report) Topology { + return r.Endpoint +} + +// SelectAddress selects the address topology. +func SelectAddress(r Report) Topology { + return r.Address +} + +// SelectContainer selects the container topology. +func SelectContainer(r Report) Topology { + return r.Container +} + // MakeReport makes a clean report, ready to Merge() other reports into. func MakeReport() Report { return Report{ diff --git a/report/topology.go b/report/topology.go index d044cff44..1e5381008 100644 --- a/report/topology.go +++ b/report/topology.go @@ -2,7 +2,6 @@ package report import ( "fmt" - "log" "net" "reflect" "strings" @@ -77,110 +76,6 @@ func NewTopology() Topology { } } -// RenderBy transforms a given Topology into a set of RenderableNodes, which -// the UI will render collectively as a graph. Note that a RenderableNode will -// always be rendered with other nodes, and therefore contains limited detail. -// -// RenderBy takes a a MapFunc, which defines how to group and label nodes. Npdes -// with the same mapped IDs will be merged. -func (t Topology) RenderBy(mapFunc MapFunc, pseudoFunc PseudoFunc) RenderableNodes { - nodes := RenderableNodes{} - - // Build a set of RenderableNodes for all non-pseudo probes, and an - // addressID to nodeID lookup map. Multiple addressIDs can map to the same - // RenderableNodes. - var ( - source2mapped = map[string]string{} // source node ID -> mapped node ID - source2host = map[string]string{} // source node ID -> origin host ID - ) - for nodeID, metadata := range t.NodeMetadatas { - mapped, ok := mapFunc(metadata) - if !ok { - continue - } - - // mapped.ID needs not be unique over all addressIDs. If not, we merge with - // the existing data, on the assumption that the MapFunc returns the same - // data. - existing, ok := nodes[mapped.ID] - if ok { - mapped.Merge(existing) - } - - mapped.Origins = mapped.Origins.Add(nodeID) - nodes[mapped.ID] = mapped - source2mapped[nodeID] = mapped.ID - source2host[nodeID] = metadata[HostNodeID] - } - - // Walk the graph and make connections. - for src, dsts := range t.Adjacency { - var ( - srcNodeID, ok = ParseAdjacencyID(src) - //srcOriginHostID, _, ok2 = ParseNodeID(srcNodeID) - srcHostNodeID = source2host[srcNodeID] - srcRenderableID = source2mapped[srcNodeID] // must exist - srcRenderableNode = nodes[srcRenderableID] // must exist - ) - if !ok { - log.Printf("bad adjacency ID %q", src) - continue - } - - for _, dstNodeID := range dsts { - dstRenderableID, ok := source2mapped[dstNodeID] - if !ok { - pseudoNode, ok := pseudoFunc(srcNodeID, srcRenderableNode, dstNodeID) - if !ok { - continue - } - dstRenderableID = pseudoNode.ID - nodes[dstRenderableID] = pseudoNode - source2mapped[dstNodeID] = dstRenderableID - } - - srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID) - srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcHostNodeID) - srcRenderableNode.Origins = srcRenderableNode.Origins.Add(srcNodeID) - edgeID := MakeEdgeID(srcNodeID, dstNodeID) - if md, ok := t.EdgeMetadatas[edgeID]; ok { - srcRenderableNode.Metadata.Merge(md.Transform()) - } - } - - nodes[srcRenderableID] = srcRenderableNode - } - - return nodes -} - -// EdgeMetadata gives the metadata of an edge from the perspective of the -// srcRenderableID. Since an edgeID can have multiple edges on the address -// level, it uses the supplied mapping function to translate address IDs to -// renderable node (mapped) IDs. -func (t Topology) EdgeMetadata(mapFunc MapFunc, srcRenderableID, dstRenderableID string) EdgeMetadata { - metadata := EdgeMetadata{} - for edgeID, edgeMeta := range t.EdgeMetadatas { - src, dst, ok := ParseEdgeID(edgeID) - if !ok { - log.Printf("bad edge ID %q", edgeID) - continue - } - if src != TheInternet { - mapped, _ := mapFunc(t.NodeMetadatas[src]) - src = mapped.ID - } - if dst != TheInternet { - mapped, _ := mapFunc(t.NodeMetadatas[dst]) - dst = mapped.ID - } - if src == srcRenderableID && dst == dstRenderableID { - metadata.Flatten(edgeMeta) - } - } - return metadata -} - // Squash squashes all non-local nodes in the topology to a super-node called // the Internet. // We rely on the values in the t.Adjacency lists being valid keys in diff --git a/report/topology_test.go b/report/topology_test.go index 0a402b5c3..fe09c3b1d 100644 --- a/report/topology_test.go +++ b/report/topology_test.go @@ -4,324 +4,8 @@ import ( "reflect" "sort" "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" ) -func init() { - spew.Config.SortKeys = true // :\ -} - -var ( - clientHostID = "client.hostname.com" - serverHostID = "server.hostname.com" - randomHostID = "random.hostname.com" - unknownHostID = "" - - clientHostNodeID = MakeHostNodeID(clientHostID) - serverHostNodeID = MakeHostNodeID(serverHostID) - randomHostNodeID = MakeHostNodeID(randomHostID) - - client54001 = MakeEndpointNodeID(clientHostID, "10.10.10.20", "54001") // curl (1) - client54002 = MakeEndpointNodeID(clientHostID, "10.10.10.20", "54002") // curl (2) - unknownClient1 = MakeEndpointNodeID(serverHostID, "10.10.10.10", "54010") // we want to ensure two unknown clients, connnected - unknownClient2 = MakeEndpointNodeID(serverHostID, "10.10.10.10", "54020") // to the same server, are deduped. - unknownClient3 = MakeEndpointNodeID(serverHostID, "10.10.10.11", "54020") // Check this one isn't deduped - server80 = MakeEndpointNodeID(serverHostID, "192.168.1.1", "80") // apache - - clientIP = MakeAddressNodeID(clientHostID, "10.10.10.20") - serverIP = MakeAddressNodeID(serverHostID, "192.168.1.1") - randomIP = MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology - unknownIP = MakeAddressNodeID(unknownHostID, "10.10.10.10") -) - -var ( - report = Report{ - Endpoint: Topology{ - Adjacency: Adjacency{ - MakeAdjacencyID(client54001): MakeIDList(server80), - MakeAdjacencyID(client54002): MakeIDList(server80), - MakeAdjacencyID(server80): MakeIDList(client54001, client54002, unknownClient1, unknownClient2, unknownClient3), - }, - NodeMetadatas: NodeMetadatas{ - // NodeMetadata is arbitrary. We're free to put only precisely what we - // care to test into the fixture. Just be sure to include the bits - // that the mapping funcs extract :) - client54001: NodeMetadata{ - "name": "curl", - "domain": "client-54001-domain", - "pid": "10001", - HostNodeID: clientHostNodeID, - }, - client54002: NodeMetadata{ - "name": "curl", // should be same as above! - "domain": "client-54002-domain", // may be different than above - "pid": "10001", // should be same as above! - HostNodeID: clientHostNodeID, - }, - server80: NodeMetadata{ - "name": "apache", - "domain": "server-80-domain", - "pid": "215", - HostNodeID: serverHostNodeID, - }, - }, - EdgeMetadatas: EdgeMetadatas{ - MakeEdgeID(client54001, server80): EdgeMetadata{ - WithBytes: true, - BytesIngress: 100, - BytesEgress: 10, - }, - MakeEdgeID(client54002, server80): EdgeMetadata{ - WithBytes: true, - BytesIngress: 200, - BytesEgress: 20, - }, - - MakeEdgeID(server80, client54001): EdgeMetadata{ - WithBytes: true, - BytesIngress: 10, - BytesEgress: 100, - }, - MakeEdgeID(server80, client54002): EdgeMetadata{ - WithBytes: true, - BytesIngress: 20, - BytesEgress: 200, - }, - MakeEdgeID(server80, unknownClient1): EdgeMetadata{ - WithBytes: true, - BytesIngress: 30, - BytesEgress: 300, - }, - MakeEdgeID(server80, unknownClient2): EdgeMetadata{ - WithBytes: true, - BytesIngress: 40, - BytesEgress: 400, - }, - MakeEdgeID(server80, unknownClient3): EdgeMetadata{ - WithBytes: true, - BytesIngress: 50, - BytesEgress: 500, - }, - }, - }, - Address: Topology{ - Adjacency: Adjacency{ - MakeAdjacencyID(clientIP): MakeIDList(serverIP), - MakeAdjacencyID(randomIP): MakeIDList(serverIP), - MakeAdjacencyID(serverIP): MakeIDList(clientIP, unknownIP), // no backlink to random - }, - NodeMetadatas: NodeMetadatas{ - clientIP: NodeMetadata{ - "name": "client.hostname.com", // hostname - HostNodeID: clientHostNodeID, - }, - randomIP: NodeMetadata{ - "name": "random.hostname.com", // hostname - HostNodeID: randomHostNodeID, - }, - serverIP: NodeMetadata{ - "name": "server.hostname.com", // hostname - HostNodeID: serverHostNodeID, - }, - }, - EdgeMetadatas: EdgeMetadatas{ - MakeEdgeID(clientIP, serverIP): EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 3, - }, - MakeEdgeID(randomIP, serverIP): EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 20, // dangling connections, weird but possible - }, - MakeEdgeID(serverIP, clientIP): EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 3, - }, - MakeEdgeID(serverIP, unknownIP): EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 7, - }, - }, - }, - } -) - -func TestRenderByEndpointPID(t *testing.T) { - want := RenderableNodes{ - "pid:client-54001-domain:10001": { - ID: "pid:client-54001-domain:10001", - LabelMajor: "curl", - LabelMinor: "client-54001-domain (10001)", - Rank: "10001", - Pseudo: false, - Adjacency: MakeIDList("pid:server-80-domain:215"), - Origins: MakeIDList(MakeHostNodeID("client.hostname.com"), MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54001")), - Metadata: AggregateMetadata{ - KeyBytesIngress: 100, - KeyBytesEgress: 10, - }, - }, - "pid:client-54002-domain:10001": { - ID: "pid:client-54002-domain:10001", - LabelMajor: "curl", - LabelMinor: "client-54002-domain (10001)", - Rank: "10001", // same process - Pseudo: false, - Adjacency: MakeIDList("pid:server-80-domain:215"), - Origins: MakeIDList(MakeHostNodeID("client.hostname.com"), MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54002")), - Metadata: AggregateMetadata{ - KeyBytesIngress: 200, - KeyBytesEgress: 20, - }, - }, - "pid:server-80-domain:215": { - ID: "pid:server-80-domain:215", - LabelMajor: "apache", - LabelMinor: "server-80-domain (215)", - Rank: "215", - Pseudo: false, - Adjacency: MakeIDList( - "pid:client-54001-domain:10001", - "pid:client-54002-domain:10001", - "pseudo;10.10.10.10;192.168.1.1;80", - "pseudo;10.10.10.11;192.168.1.1;80", - ), - Origins: MakeIDList(MakeHostNodeID("server.hostname.com"), MakeEndpointNodeID("server.hostname.com", "192.168.1.1", "80")), - Metadata: AggregateMetadata{ - KeyBytesIngress: 150, - KeyBytesEgress: 1500, - }, - }, - "pseudo;10.10.10.10;192.168.1.1;80": { - ID: "pseudo;10.10.10.10;192.168.1.1;80", - LabelMajor: "10.10.10.10", - Pseudo: true, - Metadata: AggregateMetadata{}, - }, - "pseudo;10.10.10.11;192.168.1.1;80": { - ID: "pseudo;10.10.10.11;192.168.1.1;80", - LabelMajor: "10.10.10.11", - Pseudo: true, - Metadata: AggregateMetadata{}, - }, - } - have := report.Endpoint.RenderBy(ProcessPID, GenericPseudoNode) - if !reflect.DeepEqual(want, have) { - t.Error("\n" + diff(want, have)) - } -} - -func TestRenderByEndpointPIDGrouped(t *testing.T) { - // For grouped, I've somewhat arbitrarily chosen to squash together all - // processes with the same name by removing the PID and domain (host) - // dimensions from the ID. That could be changed. - want := RenderableNodes{ - "curl": { - ID: "curl", - LabelMajor: "curl", - LabelMinor: "", - Rank: "curl", - Pseudo: false, - Adjacency: MakeIDList("apache"), - Origins: MakeIDList(MakeHostNodeID("client.hostname.com"), MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54001"), MakeEndpointNodeID("client.hostname.com", "10.10.10.20", "54002")), - Metadata: AggregateMetadata{ - KeyBytesIngress: 300, - KeyBytesEgress: 30, - }, - }, - "apache": { - ID: "apache", - LabelMajor: "apache", - LabelMinor: "", - Rank: "apache", - Pseudo: false, - Adjacency: MakeIDList( - "curl", - "pseudo;10.10.10.10;apache", - "pseudo;10.10.10.11;apache", - ), - Origins: MakeIDList(MakeHostNodeID("server.hostname.com"), MakeEndpointNodeID("server.hostname.com", "192.168.1.1", "80")), - Metadata: AggregateMetadata{ - KeyBytesIngress: 150, - KeyBytesEgress: 1500, - }, - }, - "pseudo;10.10.10.10;apache": { - ID: "pseudo;10.10.10.10;apache", - LabelMajor: "10.10.10.10", - Pseudo: true, - Metadata: AggregateMetadata{}, - }, - "pseudo;10.10.10.11;apache": { - ID: "pseudo;10.10.10.11;apache", - LabelMajor: "10.10.10.11", - Pseudo: true, - Metadata: AggregateMetadata{}, - }, - } - have := report.Endpoint.RenderBy(ProcessName, GenericGroupedPseudoNode) - if !reflect.DeepEqual(want, have) { - t.Error("\n" + diff(want, have)) - } -} - -func TestRenderByNetworkHostname(t *testing.T) { - want := RenderableNodes{ - "host:client.hostname.com": { - ID: "host:client.hostname.com", - LabelMajor: "client", // before first . - LabelMinor: "hostname.com", // after first . - Rank: "client", - Pseudo: false, - Adjacency: MakeIDList("host:server.hostname.com"), - Origins: MakeIDList(MakeHostNodeID("client.hostname.com"), MakeAddressNodeID("client.hostname.com", "10.10.10.20")), - Metadata: AggregateMetadata{ - KeyMaxConnCountTCP: 3, - }, - }, - "host:random.hostname.com": { - ID: "host:random.hostname.com", - LabelMajor: "random", // before first . - LabelMinor: "hostname.com", // after first . - Rank: "random", - Pseudo: false, - Adjacency: MakeIDList("host:server.hostname.com"), - Origins: MakeIDList(MakeHostNodeID("random.hostname.com"), MakeAddressNodeID("random.hostname.com", "172.16.11.9")), - Metadata: AggregateMetadata{ - KeyMaxConnCountTCP: 20, - }, - }, - "host:server.hostname.com": { - ID: "host:server.hostname.com", - LabelMajor: "server", // before first . - LabelMinor: "hostname.com", // after first . - Rank: "server", - Pseudo: false, - Adjacency: MakeIDList("host:client.hostname.com", "pseudo;10.10.10.10;192.168.1.1;"), - Origins: MakeIDList(MakeHostNodeID("server.hostname.com"), MakeAddressNodeID("server.hostname.com", "192.168.1.1")), - Metadata: AggregateMetadata{ - KeyMaxConnCountTCP: 10, - }, - }, - "pseudo;10.10.10.10;192.168.1.1;": { - ID: "pseudo;10.10.10.10;192.168.1.1;", - LabelMajor: "10.10.10.10", - LabelMinor: "", // after first . - Rank: "", - Pseudo: true, - Adjacency: nil, - Origins: nil, - Metadata: AggregateMetadata{}, - }, - } - have := report.Address.RenderBy(NetworkHostname, GenericPseudoNode) - if !reflect.DeepEqual(want, have) { - t.Error("\n" + diff(want, have)) - } -} - func TestTopoDiff(t *testing.T) { nodea := RenderableNode{ ID: "nodea", @@ -394,18 +78,7 @@ func TestTopoDiff(t *testing.T) { sort.Sort(ByID(c.have.Add)) sort.Sort(ByID(c.have.Update)) if !reflect.DeepEqual(c.want, c.have) { - t.Errorf("%s\n%s", c.label, diff(c.want, c.have)) + t.Errorf("%s - want:%s have:%s", c.label, c.want, c.have) } } } - -func diff(want, have interface{}) string { - text, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(spew.Sdump(want)), - B: difflib.SplitLines(spew.Sdump(have)), - FromFile: "want", - ToFile: "have", - Context: 3, - }) - return "\n" + text -}