Move mapping functions and main render function in render package.

This commit is contained in:
Tom Wilkie
2015-06-16 14:23:56 +00:00
parent 65d79d2908
commit ae9ea5c97f
9 changed files with 513 additions and 505 deletions
+194
View File
@@ -0,0 +1,194 @@
package render
import (
"fmt"
"strings"
"github.com/weaveworks/scope/report"
)
const humanTheInternet = "the Internet"
func newRenderableNode(id, major, minor, rank string) report.RenderableNode {
return report.RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: rank,
Pseudo: false,
Metadata: report.AggregateMetadata{},
}
}
func newPseudoNode(id, major, minor string) report.RenderableNode {
return report.RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: "",
Pseudo: true,
Metadata: report.AggregateMetadata{},
}
}
// MapFunc is anything which can take an arbitrary NodeMetadata, which is
// always one-to-one with nodes in a topology, and return a specific
// representation of the referenced node, in the form of a node ID and a
// human-readable major and minor labels.
//
// A single NodeMetadata can yield arbitrary many representations, including
// representations that reduce the cardinality of the set of nodes.
//
// If the final output parameter is false, the node shall be omitted from the
// rendered topology.
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 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 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"])
show = m["pid"] != "" && m["name"] != ""
)
return newRenderableNode(identifier, m["name"], minor, m["pid"]), show
}
// 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 report.NodeMetadata) (report.RenderableNode, bool) {
show := m["pid"] != "" && m["name"] != ""
return newRenderableNode(m["name"], m["name"], "", m["name"]), show
}
// MapEndpoint2Container maps endpoint topology nodes to the containers they run
// 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 report.NodeMetadata) (report.RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_container_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else {
id, major, minor, rank = m["docker_container_id"], "", m["domain"], ""
}
return newRenderableNode(id, major, minor, rank), true
}
// MapContainerIdentity maps container topology node to container mapped nodes.
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"
} else {
id, major, minor, rank = m["docker_container_id"], m["docker_container_name"], m["domain"], m["docker_image_id"]
}
return newRenderableNode(id, major, minor, rank), true
}
// 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 report.NodeMetadata) (report.RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_image_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else {
id, major, minor, rank = m["docker_image_id"], m["docker_image_name"], "", m["docker_image_id"]
}
return newRenderableNode(id, major, minor, rank), true
}
// 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 report.NodeMetadata) (report.RenderableNode, bool) {
var (
name = m["name"]
domain = ""
parts = strings.SplitN(name, ".", 2)
)
if len(parts) == 2 {
domain = parts[1]
}
return newRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0]), name != ""
}
// GenericPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) {
var maj, min, outputID string
if dst == report.TheInternet {
outputID = dst
maj, min = humanTheInternet, ""
} else {
// Rule for non-internet psuedo nodes; emit 1 new node for each
// dstNodeAddr, srcNodeAddr, srcNodePort.
srcNodeAddr, srcNodePort := trySplitAddr(src)
dstNodeAddr, _ := trySplitAddr(dst)
outputID = report.MakePseudoNodeID(dstNodeAddr, srcNodeAddr, srcNodePort)
maj, min = dstNodeAddr, ""
}
return newPseudoNode(outputID, maj, min), true
}
// GenericGroupedPseudoNode contains heuristics for building sensible pseudo nodes.
// It should go away.
func GenericGroupedPseudoNode(src string, srcMapped report.RenderableNode, dst string) (report.RenderableNode, bool) {
var maj, min, outputID string
if dst == report.TheInternet {
outputID = dst
maj, min = humanTheInternet, ""
} else {
// When grouping, emit one pseudo node per (srcNodeAddress, dstNodeAddr)
dstNodeAddr, _ := trySplitAddr(dst)
outputID = report.MakePseudoNodeID(dstNodeAddr, srcMapped.ID)
maj, min = dstNodeAddr, ""
}
return newPseudoNode(outputID, maj, min), true
}
// InternetOnlyPseudoNode never creates a pseudo node, unless it's the Internet.
func InternetOnlyPseudoNode(_ string, _ report.RenderableNode, dst string) (report.RenderableNode, bool) {
if dst == report.TheInternet {
return newPseudoNode(report.TheInternet, humanTheInternet, ""), true
}
return report.RenderableNode{}, false
}
// trySplitAddr is basically ParseArbitraryNodeID, since its callsites
// (pseudo funcs) just have opaque node IDs and don't know what topology they
// come from. Without changing how pseudo funcs work, we can't make it much
// smarter.
//
// TODO change how pseudofuncs work, and eliminate this helper.
func trySplitAddr(addr string) (string, string) {
fields := strings.SplitN(addr, report.ScopeDelim, 3)
if len(fields) == 3 {
return fields[1], fields[2]
}
if len(fields) == 2 {
return fields[1], ""
}
panic(addr)
}
+112
View File
@@ -0,0 +1,112 @@
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 report.NodeMetadata
wantOK bool
wantID, wantMajor, wantMinor, wantRank string
}{
{
f: NetworkHostname,
id: report.MakeAddressNodeID("", "1.2.3.4"),
meta: report.NodeMetadata{
"name": "my.host",
},
wantOK: true,
wantID: "host:my.host",
wantMajor: "my",
wantMinor: "host",
wantRank: "my",
},
{
f: NetworkHostname,
id: report.MakeAddressNodeID("", "1.2.3.4"),
meta: report.NodeMetadata{
"name": "localhost",
},
wantOK: true,
wantID: "host:localhost",
wantMajor: "localhost",
wantMinor: "",
wantRank: "localhost",
},
{
f: ProcessPID,
id: "not-used-beta",
meta: report.NodeMetadata{
"pid": "42",
"name": "curl",
"domain": "hosta",
},
wantOK: true,
wantID: "pid:hosta:42",
wantMajor: "curl",
wantMinor: "hosta (42)",
wantRank: "42",
},
{
f: MapEndpoint2Container,
id: "foo-id",
meta: report.NodeMetadata{
"pid": "42",
"name": "curl",
"domain": "hosta",
},
wantOK: true,
wantID: "uncontained",
wantMajor: "Uncontained",
wantMinor: "",
wantRank: "uncontained",
},
{
f: MapEndpoint2Container,
id: "bar-id",
meta: report.NodeMetadata{
"pid": "42",
"name": "curl",
"domain": "hosta",
"docker_container_id": "d321fe0",
"docker_container_name": "walking_sparrow",
"docker_image_id": "1101fff",
"docker_image_name": "org/app:latest",
},
wantOK: true,
wantID: "d321fe0",
wantMajor: "",
wantMinor: "hosta",
wantRank: "",
},
} {
identity := fmt.Sprintf("(%d %s %v)", i, c.id, c.meta)
m, haveOK := c.f(c.meta)
if want, have := c.wantOK, haveOK; want != have {
t.Errorf("%s: map OK error: want %v, have %v", identity, want, have)
}
if want, have := c.wantID, m.ID; want != have {
t.Errorf("%s: map ID error: want %#v, have %#v", identity, want, have)
}
if want, have := c.wantMajor, m.LabelMajor; want != have {
t.Errorf("%s: map major label: want %#v, have %#v", identity, want, have)
}
if want, have := c.wantMinor, m.LabelMinor; want != have {
t.Errorf("%s: map minor label: want %#v, have %#v", identity, want, have)
}
if want, have := c.wantRank, m.Rank; want != have {
t.Errorf("%s: map rank: want %#v, have %#v", identity, want, have)
}
}
}
func TestGroupedMapping(t *testing.T) {
t.Skipf("not yet implemented") // TODO
}
+110 -4
View File
@@ -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
}
+330 -4
View File
@@ -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
}