diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index 6b5aff968..3d8494a1c 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -26,15 +26,15 @@ func TestAPITopology(t *testing.T) { } if have := topology.Stats.EdgeCount; have <= 0 { - t.Errorf("EdgeCount isn't positive: %d", have) + t.Errorf("EdgeCount isn't positive for %s: %d", topology.Name, have) } if have := topology.Stats.NodeCount; have <= 0 { - t.Errorf("NodeCount isn't positive: %d", have) + t.Errorf("NodeCount isn't positive for %s: %d", topology.Name, have) } if have := topology.Stats.NonpseudoNodeCount; have <= 0 { - t.Errorf("NonpseudoNodeCount isn't positive: %d", have) + t.Errorf("NonpseudoNodeCount isn't positive for %s: %d", topology.Name, have) } } } diff --git a/app/api_topology_test.go b/app/api_topology_test.go index 3c04c28b8..91dd772af 100644 --- a/app/api_topology_test.go +++ b/app/api_topology_test.go @@ -23,36 +23,37 @@ func TestAPITopologyApplications(t *testing.T) { t.Fatal(err) } equals(t, 4, len(topo.Nodes)) - node, ok := topo.Nodes["pid:node-a.local:23128"] + node, ok := topo.Nodes["pid:hostA:23128"] if !ok { t.Errorf("missing curl node") } equals(t, 1, len(node.Adjacency)) - equals(t, report.MakeIDList("pid:node-b.local:215"), node.Adjacency) + equals(t, report.MakeIDList("pid:hostB:215"), node.Adjacency) equals(t, report.MakeIDList( report.MakeEndpointNodeID("hostA", "192.168.1.1", "12345"), report.MakeEndpointNodeID("hostA", "192.168.1.1", "12346"), + report.MakeProcessNodeID("hostA", "23128"), report.MakeHostNodeID("hostA"), ), node.Origins) equals(t, "curl", node.LabelMajor) - equals(t, "node-a.local (23128)", node.LabelMinor) + equals(t, "hostA (23128)", node.LabelMinor) equals(t, "23128", node.Rank) equals(t, false, node.Pseudo) } { - body := getRawJSON(t, ts, "/api/topology/applications/pid:node-a.local:23128") + body := getRawJSON(t, ts, "/api/topology/applications/pid:hostA:23128") var node APINode if err := json.Unmarshal(body, &node); err != nil { t.Fatal(err) } - equals(t, "pid:node-a.local:23128", node.Node.ID) + equals(t, "pid:hostA:23128", node.Node.ID) equals(t, "curl", node.Node.LabelMajor) - equals(t, "node-a.local (23128)", node.Node.LabelMinor) + equals(t, "hostA (23128)", node.Node.LabelMinor) equals(t, false, node.Node.Pseudo) // Let's not unit-test the specific content of the detail tables } { - body := getRawJSON(t, ts, "/api/topology/applications/pid:node-a.local:23128/pid:node-b.local:215") + body := getRawJSON(t, ts, "/api/topology/applications/pid:hostA:23128/pid:hostB:215") var edge APIEdge if err := json.Unmarshal(body, &edge); err != nil { t.Fatalf("JSON parse error: %s", err) diff --git a/app/mock_reporter_test.go b/app/mock_reporter_test.go index 5529b52da..cbcba0c84 100644 --- a/app/mock_reporter_test.go +++ b/app/mock_reporter_test.go @@ -55,32 +55,63 @@ func (s StaticReport) Report() report.Report { }, NodeMetadatas: report.NodeMetadatas{ report.MakeEndpointNodeID("hostA", "192.168.1.1", "12345"): report.NodeMetadata{ + "addr": "192.168.1.1", + "port": "12345", "pid": "23128", - "name": "curl", - "domain": "node-a.local", report.HostNodeID: report.MakeHostNodeID("hostA"), }, report.MakeEndpointNodeID("hostA", "192.168.1.1", "12346"): report.NodeMetadata{ // <-- same as :12345 + "addr": "192.168.1.1", + "port": "12346", "pid": "23128", - "name": "curl", - "domain": "node-a.local", report.HostNodeID: report.MakeHostNodeID("hostA"), }, report.MakeEndpointNodeID("hostA", "192.168.1.1", "8888"): report.NodeMetadata{ + "addr": "192.168.1.1", + "port": "8888", "pid": "55100", - "name": "ssh", - "domain": "node-a.local", report.HostNodeID: report.MakeHostNodeID("hostA"), }, report.MakeEndpointNodeID("hostB", "192.168.1.2", "80"): report.NodeMetadata{ + "addr": "192.168.1.2", + "port": "80", "pid": "215", - "name": "apache", - "domain": "node-b.local", report.HostNodeID: report.MakeHostNodeID("hostB"), }, }, }, + Process: report.Topology{ + NodeMetadatas: report.NodeMetadatas{ + report.MakeProcessNodeID("hostA", "23128"): report.NodeMetadata{ + "pid": "23128", + "comm": "curl", + report.HostNodeID: report.MakeHostNodeID("hostA"), + }, + report.MakeProcessNodeID("hostA", "8888"): report.NodeMetadata{ + "pid": "8888", + "comm": "ssh", + report.HostNodeID: report.MakeHostNodeID("hostA"), + }, + report.MakeProcessNodeID("hostB", "80"): report.NodeMetadata{ + "pid": "80", + "comm": "apache", + "docker_container_id": "abcdefg", + report.HostNodeID: report.MakeHostNodeID("hostB"), + }, + }, + }, + + Container: report.Topology{ + NodeMetadatas: report.NodeMetadatas{ + report.MakeContainerNodeID("hostB", "abcdefg"): report.NodeMetadata{ + "docker_container_id": "abcdefg", + "docker_container_name": "server", + report.HostNodeID: report.MakeHostNodeID("hostB"), + }, + }, + }, + Address: report.Topology{ Adjacency: report.Adjacency{ report.MakeAdjacencyID(report.MakeAddressNodeID("hostA", "192.168.1.1")): report.MakeIDList(report.MakeAddressNodeID("hostB", "192.168.1.2"), report.MakeAddressNodeID("", "1.2.3.4")), diff --git a/app/router.go b/app/router.go index 4fa40f517..581d144b6 100644 --- a/app/router.go +++ b/app/router.go @@ -50,30 +50,27 @@ var topologyRegistry = map[string]topologyView{ "applications": { human: "Applications", parent: "", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessPID, Pseudo: render.GenericPseudoNode}, + renderer: render.FilterUnconnected{Renderer: render.ProcessRenderer}, }, "applications-by-name": { human: "by name", parent: "applications", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessName, Pseudo: render.GenericGroupedPseudoNode}, + renderer: render.FilterUnconnected{Renderer: render.ProcessNameRenderer}, }, "containers": { - human: "Containers", - parent: "", - renderer: render.Reduce([]render.Renderer{ - render.Map{Selector: report.SelectEndpoint, Mapper: render.MapEndpoint2Container, Pseudo: render.InternetOnlyPseudoNode}, - render.Map{Selector: report.SelectContainer, Mapper: render.MapContainerIdentity, Pseudo: render.InternetOnlyPseudoNode}, - }), + human: "Containers", + parent: "", + renderer: render.ContainerRenderer, }, "containers-by-image": { human: "by image", parent: "containers", - renderer: render.Map{Selector: report.SelectEndpoint, Mapper: render.ProcessContainerImage, Pseudo: render.InternetOnlyPseudoNode}, + renderer: render.LeafMap{Selector: report.SelectEndpoint, Mapper: render.ProcessContainerImage, Pseudo: render.InternetOnlyPseudoNode}, }, "hosts": { human: "Hosts", parent: "", - renderer: render.Map{Selector: report.SelectAddress, Mapper: render.NetworkHostname, Pseudo: render.GenericPseudoNode}, + renderer: render.LeafMap{Selector: report.SelectAddress, Mapper: render.NetworkHostname, Pseudo: render.GenericPseudoNode}, }, } diff --git a/experimental/graphviz/handle.go b/experimental/graphviz/handle.go index 5dc611a94..03c4e65a8 100644 --- a/experimental/graphviz/handle.go +++ b/experimental/graphviz/handle.go @@ -16,7 +16,7 @@ func handleTXT(r Reporter) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "text/plain") - renderer := render.Map{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} + renderer := render.LeafMap{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} dot(w, renderer.Render(r.Report())) //report.Render(r.Report(), report.SelectEndpoint, mapFunc(req), report.NoPseudoNode)) @@ -35,7 +35,7 @@ func handleSVG(r Reporter) http.HandlerFunc { cmd.Stdout = w - renderer := render.Map{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} + renderer := render.LeafMap{Selector: report.SelectEndpoint, Mapper: mapFunc(req), Pseudo: nil} dot(wc, renderer.Render(r.Report())) wc.Close() @@ -102,12 +102,12 @@ func engine(r *http.Request) string { return engine } -func mapFunc(r *http.Request) render.MapFunc { +func mapFunc(r *http.Request) render.LeafMapFunc { switch strings.ToLower(r.FormValue("map_func")) { case "hosts", "networkhost", "networkhostname": return render.NetworkHostname } - return render.ProcessPID + return render.MapProcessIdentity } func classView(r *http.Request) bool { diff --git a/probe/spy.go b/probe/spy.go index aa7b03ef4..72e6bcb24 100644 --- a/probe/spy.go +++ b/probe/spy.go @@ -76,9 +76,9 @@ func addConnection( if _, ok := r.Endpoint.NodeMetadatas[scopedLocal]; !ok { // First hit establishes NodeMetadata for scoped local address + port md := report.NodeMetadata{ - "pid": fmt.Sprintf("%d", c.Proc.PID), - "name": c.Proc.Name, - "domain": hostID, + "addr": c.LocalAddress.String(), + "port": strconv.Itoa(int(c.LocalPort)), + "pid": fmt.Sprintf("%d", c.Proc.PID), } r.Endpoint.NodeMetadatas[scopedLocal] = md diff --git a/probe/spy_test.go b/probe/spy_test.go index c2574c7d4..3ef351381 100644 --- a/probe/spy_test.go +++ b/probe/spy_test.go @@ -124,9 +124,7 @@ func TestSpyWithProcesses(t *testing.T) { } for key, want := range map[string]string{ - "domain": nodeID, - "name": fixProcessName, - "pid": strconv.FormatUint(uint64(fixProcessPID), 10), + "pid": strconv.FormatUint(uint64(fixProcessPID), 10), } { if have := r.Endpoint.NodeMetadatas[scopedLocal][key]; want != have { t.Errorf("Process.NodeMetadatas[%q][%q]: want %q, have %q", scopedLocal, key, want, have) diff --git a/probe/tag/docker_tagger.go b/probe/tag/docker_tagger.go index 7fd3059bc..8455818a4 100644 --- a/probe/tag/docker_tagger.go +++ b/probe/tag/docker_tagger.go @@ -271,7 +271,13 @@ func (t *DockerTagger) Containers() []*docker.Container { // Tag implements Tagger. func (t *DockerTagger) Tag(r report.Report) report.Report { - for nodeID, nodeMetadata := range r.Endpoint.NodeMetadatas { + t.tag(&r.Process) + t.tag(&r.Endpoint) + return r +} + +func (t *DockerTagger) tag(topology *report.Topology) { + for nodeID, nodeMetadata := range topology.NodeMetadatas { pidStr, ok := nodeMetadata["pid"] if !ok { //log.Printf("dockerTagger: %q: no process node ID", id) @@ -318,10 +324,8 @@ func (t *DockerTagger) Tag(r report.Report) report.Report { md[ImageName] = image.RepoTags[0] } - r.Endpoint.NodeMetadatas[nodeID].Merge(md) + topology.NodeMetadatas[nodeID].Merge(md) } - - return r } // ContainerTopology produces a Toplogy of Containers diff --git a/probe/tag/docker_tagger_test.go b/probe/tag/docker_tagger_test.go index dde9ff136..ce25784f3 100644 --- a/probe/tag/docker_tagger_test.go +++ b/probe/tag/docker_tagger_test.go @@ -67,8 +67,8 @@ func TestDockerTagger(t *testing.T) { } var ( - endpoint1NodeID = "somehost.com;192.168.1.1;12345" - endpoint2NodeID = "somehost.com;192.168.1.1;67890" + pid1NodeID = report.MakeProcessNodeID("somehost.com", "1") + pid2NodeID = report.MakeProcessNodeID("somehost.com", "2") endpointNodeMetadata = report.NodeMetadata{ ContainerID: "foo", ImageID: "baz", @@ -83,17 +83,17 @@ func TestDockerTagger(t *testing.T) { ) r := report.MakeReport() - r.Endpoint.NodeMetadatas[endpoint1NodeID] = report.NodeMetadata{"pid": "1"} - r.Endpoint.NodeMetadatas[endpoint2NodeID] = report.NodeMetadata{"pid": "2"} + r.Process.NodeMetadatas[pid1NodeID] = report.NodeMetadata{"pid": "1"} + r.Process.NodeMetadatas[pid2NodeID] = report.NodeMetadata{"pid": "2"} dockerTagger, _ := NewDockerTagger("/irrelevant", 10*time.Second) runtime.Gosched() - for _, endpointNodeID := range []string{endpoint1NodeID, endpoint2NodeID} { + for _, nodeID := range []string{pid1NodeID, pid2NodeID} { want := endpointNodeMetadata.Copy() - have := dockerTagger.Tag(r).Endpoint.NodeMetadatas[endpointNodeID].Copy() + have := dockerTagger.Tag(r).Process.NodeMetadatas[nodeID].Copy() delete(have, "pid") if !reflect.DeepEqual(want, have) { - t.Errorf("%q: want %+v, have %+v", endpointNodeID, want, have) + t.Errorf("%q: want %+v, have %+v", nodeID, want, have) } } diff --git a/render/detailed_node.go b/render/detailed_node.go index 4552a7823..6b3e9513f 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -38,13 +38,13 @@ func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { tables := []Table{} { rows := []Row{} - if val, ok := n.Metadata[report.KeyMaxConnCountTCP]; ok { + if val, ok := n.AggregateMetadata[report.KeyMaxConnCountTCP]; ok { rows = append(rows, Row{"TCP connections", strconv.FormatInt(int64(val), 10), ""}) } - if val, ok := n.Metadata[report.KeyBytesIngress]; ok { + if val, ok := n.AggregateMetadata[report.KeyBytesIngress]; ok { rows = append(rows, Row{"Bytes ingress", strconv.FormatInt(int64(val), 10), ""}) } - if val, ok := n.Metadata[report.KeyBytesEgress]; ok { + if val, ok := n.AggregateMetadata[report.KeyBytesEgress]; ok { rows = append(rows, Row{"Bytes egress", strconv.FormatInt(int64(val), 10), ""}) } if len(rows) > 0 { @@ -102,10 +102,8 @@ func OriginTable(r report.Report, originID string) (Table, bool) { func endpointOriginTable(nmd report.NodeMetadata) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ - {"endpoint", "Endpoint"}, - {"host_name", "Host name"}, - {"pid", "PID"}, - {"name", "Process name"}, + {"addr", "Endpoint"}, + {"port", "Port"}, } { if val, ok := nmd[tuple.key]; ok { rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""}) diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go index 0ca88387f..c9f882eba 100644 --- a/render/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -5,13 +5,8 @@ import ( "testing" "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/report" ) -func TestMakeDetailedNode(t *testing.T) { - t.Skip("TODO") -} - func TestOriginTable(t *testing.T) { if _, ok := render.OriginTable(rpt, "not-found"); ok { t.Errorf("unknown origin ID gave unexpected success") @@ -21,9 +16,8 @@ func TestOriginTable(t *testing.T) { Title: "Origin Endpoint", Numeric: false, Rows: []render.Row{ - {"Host name", clientHostName, ""}, - {"PID", "10001", ""}, - {"Process name", "curl", ""}, + {"Endpoint", clientIP, ""}, + {"Port", clientPort54001, ""}, }, }, clientAddressNodeID: { @@ -33,12 +27,12 @@ func TestOriginTable(t *testing.T) { {"Host name", clientHostName, ""}, }, }, - report.MakeProcessNodeID(clientHostID, "4242"): { + serverProcessNodeID: { Title: "Origin Process", Numeric: false, Rows: []render.Row{ - {"Name (comm)", "curl", ""}, - {"PID", "4242", ""}, + {"Name (comm)", "apache", ""}, + {"PID", serverPID, ""}, }, }, serverHostNodeID: { @@ -61,3 +55,60 @@ func TestOriginTable(t *testing.T) { } } } + +func TestMakeDetailedNode(t *testing.T) { + renderableNode := render.ContainerRenderer.Render(rpt)[serverContainerID] + have := render.MakeDetailedNode(rpt, renderableNode) + want := render.DetailedNode{ + ID: serverContainerID, + LabelMajor: "server", + LabelMinor: serverHostName, + Pseudo: false, + Tables: []render.Table{ + { + Title: "Connections", + Numeric: true, + Rows: []render.Row{ + {"Bytes ingress", "150", ""}, + {"Bytes egress", "1500", ""}, + }, + }, + { + Title: "Origin Endpoint", + Numeric: false, + Rows: []render.Row{ + {"Endpoint", "192.168.1.1", ""}, + {"Port", "80", ""}, + }, + }, + { + Title: "Origin Process", + Numeric: false, + Rows: []render.Row{ + {"Name (comm)", "apache", ""}, + {"PID", "215", ""}, + }, + }, + { + Title: "Origin Container", + Numeric: false, + Rows: []render.Row{ + {"Container ID", "5e4d3c2b1a", ""}, + {"Container name", "server", ""}, + }, + }, + { + Title: "Origin Host", + Numeric: false, + Rows: []render.Row{ + {"Host name", "server.hostname.com", ""}, + {"Load", "0.01 0.01 0.01", ""}, + {"Operating system", "Linux", ""}, + }, + }, + }, + } + if !reflect.DeepEqual(want, have) { + t.Errorf("%s", diff(want, have)) + } +} diff --git a/render/mapping.go b/render/mapping.go index e1973a85b..444591fc6 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -7,31 +7,15 @@ import ( "github.com/weaveworks/scope/report" ) -const humanTheInternet = "the Internet" +// Constants are used in the tests. +const ( + UncontainedID = "uncontained" + UncontainedMajor = "Uncontained" -func newRenderableNode(id, major, minor, rank string) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: major, - LabelMinor: minor, - Rank: rank, - Pseudo: false, - Metadata: report.AggregateMetadata{}, - } -} + humanTheInternet = "the Internet" +) -func newPseudoNode(id, major, minor string) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: major, - LabelMinor: minor, - Rank: "", - Pseudo: true, - Metadata: report.AggregateMetadata{}, - } -} - -// MapFunc is anything which can take an arbitrary NodeMetadata, which is +// LeafMapFunc 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. @@ -41,7 +25,7 @@ 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(report.NodeMetadata) (RenderableNode, bool) +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 @@ -49,52 +33,127 @@ type MapFunc func(report.NodeMetadata) (RenderableNode, bool) // node IDs prior to mapping. type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string) (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) (RenderableNode, bool) { +// MapFunc is anything which can take an arbitrary RenderableNode and +// return another RenderableNode. +// +// As with LeafMapFunc, if the final output parameter is false, the node +// shall be omitted from the rendered topology. +type MapFunc func(RenderableNode) (RenderableNode, bool) + +// MapEndpointIdentity maps a endpoint topology node to endpoint RenderableNode +// node. As it is only ever run on endpoint topology nodes, we can safely +// assume the presence of certain keys. +func MapEndpointIdentity(m report.NodeMetadata) (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"] != "" + id = fmt.Sprintf("endpoint:%s:%s:%s", report.ExtractHostID(m), m["addr"], m["port"]) + major = fmt.Sprintf("%s:%s", m["addr"], m["port"]) + pid, ok = m["pid"] + minor = report.ExtractHostID(m) + rank = major ) - 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) (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) (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"], "" + if ok { + minor = fmt.Sprintf("%s (%s)", report.ExtractHostID(m), pid) } - return newRenderableNode(id, major, minor, rank), true + return NewRenderableNode(id, major, minor, rank, m), true } -// MapContainerIdentity maps container topology node to container mapped nodes. +// MapProcessIdentity maps a process topology node to process RenderableNode node. +// As it is only ever run on process topology nodes, we can safely assume the +// presence of certain keys. +func MapProcessIdentity(m report.NodeMetadata) (RenderableNode, bool) { + var ( + id = fmt.Sprintf("pid:%s:%s", report.ExtractHostID(m), m["pid"]) + major = m["comm"] + minor = fmt.Sprintf("%s (%s)", report.ExtractHostID(m), m["pid"]) + rank = m["pid"] + ) + + return NewRenderableNode(id, major, minor, rank, m), true +} + +// MapContainerIdentity maps a container topology node to a container +// RenderableNode node. As it is only ever run on container topology +// nodes, we can safely assume the presences of certain keys. func MapContainerIdentity(m report.NodeMetadata) (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"] + var ( + id = m["docker_container_id"] + major = m["docker_container_name"] + minor = report.ExtractHostID(m) + rank = m["docker_image_id"] + ) + + return NewRenderableNode(id, major, minor, rank, m), true +} + +// MapEndpoint2Process maps endpoint RenderableNodes to process +// RenderableNodes. +// +// If this function is given a pseudo node, then it will just return it; +// Pseudo nodes will never have pids in them, and therefore will never +// be able to be turned into a Process node. +// +// Otherwise, this function will produce a node with the correct ID +// format for a process, but without any Major or Minor labels. +// It does not have enough info to do that, and the resulting graph +// must be merged with a process graph to get that info. +func MapEndpoint2Process(n RenderableNode) (RenderableNode, bool) { + if n.Pseudo { + return n, true } - return newRenderableNode(id, major, minor, rank), true + pid, ok := n.NodeMetadata["pid"] + if !ok { + // TODO: Propogate a pseudo node instead of dropping this? + return RenderableNode{}, false + } + + id := fmt.Sprintf("pid:%s:%s", report.ExtractHostID(n.NodeMetadata), pid) + return newDerivedNode(id, n), true +} + +// MapProcess2Container maps process RenderableNodes to container +// RenderableNodes. +// +// If this function is given a node without a docker_container_id +// (including other pseudo nodes), it will produce an "Uncontained" +// pseudo node. +// +// Otherwise, this function will produce a node with the correct ID +// format for a container, but without any Major or Minor labels. +// It does not have enough info to do that, and the resulting graph +// must be merged with a container graph to get that info. +func MapProcess2Container(n RenderableNode) (RenderableNode, bool) { + id, ok := n.NodeMetadata["docker_container_id"] + if !ok || n.Pseudo { + return newDerivedPseudoNode(UncontainedID, UncontainedMajor, n), true + } + + return newDerivedNode(id, n), true +} + +// MapProcess2Name maps process RenderableNodes to RenderableNodes +// for each process name. +// +// This mapper is unlike the other foo2bar mappers as the intention +// is not to join the information with another topology. Therefore +// it outputs a properly-formed node with labels etc. +func MapProcess2Name(n RenderableNode) (RenderableNode, bool) { + if n.Pseudo { + return n, true + } + + name, ok := n.NodeMetadata["comm"] + if !ok { + // TODO: Propogate a pseudo node instead of dropping this? + return RenderableNode{}, false + } + + node := newDerivedNode(name, n) + node.LabelMajor = name + node.Rank = name + return node, true } // ProcessContainerImage maps topology nodes to the container images they run @@ -103,12 +162,12 @@ func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) { func ProcessContainerImage(m report.NodeMetadata) (RenderableNode, bool) { var id, major, minor, rank string if m["docker_image_id"] == "" { - id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained" + id, major, minor, rank = UncontainedID, UncontainedMajor, "", UncontainedID } else { id, major, minor, rank = m["docker_image_id"], m["docker_image_name"], "", m["docker_image_id"] } - return newRenderableNode(id, major, minor, rank), true + return NewRenderableNode(id, major, minor, rank, m), true } // NetworkHostname takes a node NodeMetadata and returns a representation @@ -125,7 +184,7 @@ func NetworkHostname(m report.NodeMetadata) (RenderableNode, bool) { domain = parts[1] } - return newRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0]), name != "" + return NewRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0], m), name != "" } // GenericPseudoNode contains heuristics for building sensible pseudo nodes. diff --git a/render/mapping_test.go b/render/mapping_test.go index 7579e6074..2d2cbabdb 100644 --- a/render/mapping_test.go +++ b/render/mapping_test.go @@ -10,7 +10,7 @@ import ( func TestUngroupedMapping(t *testing.T) { for i, c := range []struct { - f render.MapFunc + f render.LeafMapFunc id string meta report.NodeMetadata wantOK bool @@ -40,52 +40,6 @@ func TestUngroupedMapping(t *testing.T) { wantMinor: "", wantRank: "localhost", }, - { - f: render.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: render.MapEndpoint2Container, - id: "foo-id", - meta: report.NodeMetadata{ - "pid": "42", - "name": "curl", - "domain": "hosta", - }, - wantOK: true, - wantID: "uncontained", - wantMajor: "Uncontained", - wantMinor: "", - wantRank: "uncontained", - }, - { - f: render.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) diff --git a/render/render.go b/render/render.go index f89843683..97d54ce03 100644 --- a/render/render.go +++ b/render/render.go @@ -6,17 +6,42 @@ import ( "github.com/weaveworks/scope/report" ) -// Renderer is something that can render a report to a set of RenderableNodes +// Renderer is something that can render a report to a set of RenderableNodes. type Renderer interface { Render(report.Report) RenderableNodes AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata } // Reduce renderer is a Renderer which merges together the output of several -// other renderers +// other renderers. type Reduce []Renderer -// Render produces a set of RenderableNodes given a Report +// Map is a Renderer which produces a set of RenderableNodes from the set of +// RenderableNodes produced by another Renderer. +type Map struct { + MapFunc + Renderer +} + +// LeafMap is a Renderer which produces a set of RenderableNodes from a report.Topology +// by using a map function and topology selector. +type LeafMap struct { + Selector report.TopologySelector + Mapper LeafMapFunc + Pseudo PseudoFunc +} + +// FilterUnconnected is a Renderer which filters out unconnected nodes. +type FilterUnconnected struct { + Renderer +} + +// MakeReduce is the only sane way to produce a Reduce Renderer. +func MakeReduce(renderers ...Renderer) Renderer { + return Reduce(renderers) +} + +// Render produces a set of RenderableNodes given a Report. func (r Reduce) Render(rpt report.Report) RenderableNodes { result := RenderableNodes{} for _, renderer := range r { @@ -25,7 +50,7 @@ func (r Reduce) Render(rpt report.Report) RenderableNodes { return result } -// AggregateMetadata produces an AggregateMetadata for a given edge +// AggregateMetadata produces an AggregateMetadata for a given edge. func (r Reduce) AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata { metadata := report.AggregateMetadata{} for _, renderer := range r { @@ -34,37 +59,100 @@ func (r Reduce) AggregateMetadata(rpt report.Report, localID, remoteID string) r return metadata } -// Map is a Renderer which produces a set of RendererNodes by using a -// Mapper functions and topology selector. -type Map struct { - Selector report.TopologySelector - Mapper MapFunc - Pseudo PseudoFunc -} - -// Render produces a set of RenderableNodes given a Report +// Render transforms a set of RenderableNodes produces by another Renderer. +// using a map function func (m Map) Render(rpt report.Report) RenderableNodes { - return Topology(m.Selector(rpt), m.Mapper, m.Pseudo) + output, _ := m.render(rpt) + return output } -// Topology transforms a given Topology into a set of RenderableNodes, which +func (m Map) render(rpt report.Report) (RenderableNodes, map[string]string) { + input := m.Renderer.Render(rpt) + output := RenderableNodes{} + mapped := map[string]string{} // input node ID -> output node ID + adjacencies := map[string]report.IDList{} // output node ID -> input node Adjacencies + + for _, inRenderable := range input { + outRenderable, ok := m.MapFunc(inRenderable) + if !ok { + continue + } + + existing, ok := output[outRenderable.ID] + if ok { + outRenderable.Merge(existing) + } + + output[outRenderable.ID] = outRenderable + mapped[inRenderable.ID] = outRenderable.ID + adjacencies[outRenderable.ID] = adjacencies[outRenderable.ID].Add(inRenderable.Adjacency...) + } + + // Rewrite Adjacency for new node IDs. + // NB we don't do pseudo nodes here; we assume the input graph + // is properly-connected, and if the map func dropped a node, + // we drop links to it. + for outNodeID, inAdjacency := range adjacencies { + outAdjacency := report.MakeIDList() + for _, inAdjacent := range inAdjacency { + if outAdjacent, ok := mapped[inAdjacent]; ok { + outAdjacency = outAdjacency.Add(outAdjacent) + } + } + outNode := output[outNodeID] + outNode.Adjacency = outAdjacency + output[outNodeID] = outNode + } + + return output, mapped +} + +// AggregateMetadata 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 (m Map) AggregateMetadata(rpt report.Report, srcRenderableID, dstRenderableID string) report.AggregateMetadata { + // First we need to map the ids in this layer into the ids in the underlying layer + _, mapped := m.render(rpt) // this maps from old -> new + inverted := map[string][]string{} // this maps from new -> old(s) + for k, v := range mapped { + existing := inverted[v] + existing = append(existing, k) + inverted[v] = existing + } + + // Now work out a slice of edges this edge is constructed from + oldEdges := []struct{ src, dst string }{} + for _, oldSrcID := range inverted[srcRenderableID] { + for _, oldDstID := range inverted[dstRenderableID] { + oldEdges = append(oldEdges, struct{ src, dst string }{oldSrcID, oldDstID}) + } + } + + // Now recurse for each old edge + output := report.AggregateMetadata{} + for _, edge := range oldEdges { + metadata := m.Renderer.AggregateMetadata(rpt, edge.src, edge.dst) + output.Merge(metadata) + } + return output +} + +// Render transforms a given Report 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 Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) RenderableNodes { +// Nodes with the same mapped IDs will be merged. +func (m LeafMap) Render(rpt report.Report) RenderableNodes { + t := m.Selector(rpt) 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 - ) + source2mapped := map[string]string{} // source node ID -> mapped node ID for nodeID, metadata := range t.NodeMetadatas { - mapped, ok := mapFunc(metadata) + mapped, ok := m.Mapper(metadata) if !ok { continue } @@ -77,18 +165,19 @@ func Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) Rendera mapped.Merge(existing) } - mapped.Origins = mapped.Origins.Add(nodeID) + origins := mapped.Origins + origins = origins.Add(nodeID) + origins = origins.Add(metadata[report.HostNodeID]) + mapped.Origins = origins + 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] + srcNodeID, ok = report.ParseAdjacencyID(src) srcRenderableID = source2mapped[srcNodeID] // must exist srcRenderableNode = nodes[srcRenderableID] // must exist ) @@ -100,7 +189,7 @@ func Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) Rendera for _, dstNodeID := range dsts { dstRenderableID, ok := source2mapped[dstNodeID] if !ok { - pseudoNode, ok := pseudoFunc(srcNodeID, srcRenderableNode, dstNodeID) + pseudoNode, ok := m.Pseudo(srcNodeID, srcRenderableNode, dstNodeID) if !ok { continue } @@ -110,11 +199,10 @@ func Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) Rendera } 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()) + srcRenderableNode.AggregateMetadata.Merge(md.Transform()) } } @@ -124,16 +212,12 @@ func Topology(t report.Topology, mapFunc MapFunc, pseudoFunc PseudoFunc) Rendera return nodes } -// AggregateMetadata produces an AggregateMetadata for a given edge -func (m Map) AggregateMetadata(rpt report.Report, localID, remoteID string) report.AggregateMetadata { - return edgeMetadata(m.Selector(rpt), m.Mapper, localID, remoteID).Transform() -} - -// EdgeMetadata gives the metadata of an edge from the perspective of the +// AggregateMetadata 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 { +func (m LeafMap) AggregateMetadata(rpt report.Report, srcRenderableID, dstRenderableID string) report.AggregateMetadata { + t := m.Selector(rpt) metadata := report.EdgeMetadata{} for edgeID, edgeMeta := range t.EdgeMetadatas { src, dst, ok := report.ParseEdgeID(edgeID) @@ -142,16 +226,33 @@ func edgeMetadata(t report.Topology, mapFunc MapFunc, srcRenderableID, dstRender continue } if src != report.TheInternet { - mapped, _ := mapFunc(t.NodeMetadatas[src]) + mapped, _ := m.Mapper(t.NodeMetadatas[src]) src = mapped.ID } if dst != report.TheInternet { - mapped, _ := mapFunc(t.NodeMetadatas[dst]) + mapped, _ := m.Mapper(t.NodeMetadatas[dst]) dst = mapped.ID } if src == srcRenderableID && dst == dstRenderableID { metadata.Flatten(edgeMeta) } } - return metadata + return metadata.Transform() +} + +// Render produces a set of RenderableNodes given a Report +func (f FilterUnconnected) Render(rpt report.Report) RenderableNodes { + input := f.Renderer.Render(rpt) + output := RenderableNodes{} + for id, node := range input { + if len(node.Adjacency) == 0 { + continue + } + + output[id] = node + for _, id := range node.Adjacency { + output[id] = input[id] + } + } + return output } diff --git a/render/render_test.go b/render/render_test.go index a144e6e75..b0ff17399 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -6,15 +6,8 @@ import ( "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 { render.RenderableNodes aggregateMetadata report.AggregateMetadata @@ -55,360 +48,121 @@ func TestReduceEdge(t *testing.T) { } } -var ( - clientHostID = "client.hostname.com" - serverHostID = "server.hostname.com" - randomHostID = "random.hostname.com" - unknownHostID = "" +func TestMapRender1(t *testing.T) { + // 1. Check when we return false, the node gets filtered out + mapper := render.Map{ + MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) { + return render.RenderableNode{}, false + }, + Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ + "foo": {ID: "foo"}, + }}, + } + want := render.RenderableNodes{} + have := mapper.Render(report.MakeReport()) + if !reflect.DeepEqual(want, have) { + t.Errorf("want %+v, have %+v", want, have) + } +} - clientHostName = clientHostID - serverHostName = serverHostID +func TestMapRender2(t *testing.T) { + // 2. Check we can remap two nodes into one + mapper := render.Map{ + MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) { + return render.RenderableNode{ID: "bar"}, true + }, + Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ + "foo": {ID: "foo"}, + "baz": {ID: "baz"}, + }}, + } + want := render.RenderableNodes{ + "bar": render.RenderableNode{ID: "bar"}, + } + have := mapper.Render(report.MakeReport()) + if !reflect.DeepEqual(want, have) { + t.Errorf("want %+v, have %+v", want, have) + } +} - clientHostNodeID = report.MakeHostNodeID(clientHostID) - serverHostNodeID = report.MakeHostNodeID(serverHostID) - randomHostNodeID = report.MakeHostNodeID(randomHostID) +func TestMapRender3(t *testing.T) { + // 3. Check we can remap adjacencies + mapper := render.Map{ + MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) { + return render.RenderableNode{ID: "_" + nodes.ID}, true + }, + Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ + "foo": {ID: "foo", Adjacency: report.MakeIDList("baz")}, + "baz": {ID: "baz", Adjacency: report.MakeIDList("foo")}, + }}, + } + want := render.RenderableNodes{ + "_foo": {ID: "_foo", Adjacency: report.MakeIDList("_baz")}, + "_baz": {ID: "_baz", Adjacency: report.MakeIDList("_foo")}, + } + have := mapper.Render(report.MakeReport()) + if !reflect.DeepEqual(want, have) { + t.Errorf("want %+v, have %+v", want, have) + } +} - client54001NodeID = report.MakeEndpointNodeID(clientHostID, "10.10.10.20", "54001") // curl (1) - client54002NodeID = 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 - - clientAddressNodeID = report.MakeAddressNodeID(clientHostID, "10.10.10.20") - serverAddressNodeID = report.MakeAddressNodeID(serverHostID, "192.168.1.1") - randomAddressNodeID = report.MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology - unknownAddressNodeID = report.MakeAddressNodeID(unknownHostID, "10.10.10.10") -) - -var ( - rpt = report.Report{ - Endpoint: report.Topology{ - Adjacency: report.Adjacency{ - report.MakeAdjacencyID(client54001NodeID): report.MakeIDList(server80), - report.MakeAdjacencyID(client54002NodeID): report.MakeIDList(server80), - report.MakeAdjacencyID(server80): report.MakeIDList(client54001NodeID, client54002NodeID, unknownClient1, unknownClient2, unknownClient3), - }, +func TestMapEdge(t *testing.T) { + selector := func(_ report.Report) report.Topology { + return report.Topology{ 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 :) - client54001NodeID: report.NodeMetadata{ - "name": "curl", - "domain": "client-54001-domain", - "pid": "10001", - report.HostNodeID: clientHostNodeID, - "host_name": clientHostName, - }, - client54002NodeID: 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, - }, + "foo": report.NodeMetadata{"id": "foo"}, + "bar": report.NodeMetadata{"id": "bar"}, + }, + Adjacency: report.Adjacency{ + ">foo": report.MakeIDList("bar"), + ">bar": report.MakeIDList("foo"), }, EdgeMetadatas: report.EdgeMetadatas{ - report.MakeEdgeID(client54001NodeID, server80): report.EdgeMetadata{ - WithBytes: true, - BytesIngress: 100, - BytesEgress: 10, - }, - report.MakeEdgeID(client54002NodeID, server80): report.EdgeMetadata{ - WithBytes: true, - BytesIngress: 200, - BytesEgress: 20, - }, + "foo|bar": report.EdgeMetadata{WithBytes: true, BytesIngress: 1, BytesEgress: 2}, + "bar|foo": report.EdgeMetadata{WithBytes: true, BytesIngress: 3, BytesEgress: 4}, + }, + } + } - report.MakeEdgeID(server80, client54001NodeID): report.EdgeMetadata{ - WithBytes: true, - BytesIngress: 10, - BytesEgress: 100, - }, - report.MakeEdgeID(server80, client54002NodeID): 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, - }, - }, - }, - Process: report.Topology{ - Adjacency: report.Adjacency{}, - NodeMetadatas: report.NodeMetadatas{ - report.MakeProcessNodeID(clientHostID, "4242"): report.NodeMetadata{ - "host_name": "client.host.com", - "pid": "4242", - "comm": "curl", - "docker_container_id": "a1b2c3d4e5", - "docker_container_name": "fixture-container", - "docker_image_id": "0000000000", - "docker_image_name": "fixture/container:latest", - }, - report.MakeProcessNodeID(serverHostID, "215"): report.NodeMetadata{ - "pid": "215", - "process_name": "apache", - }, + identity := func(nmd report.NodeMetadata) (render.RenderableNode, bool) { + return render.NewRenderableNode(nmd["id"], "", "", "", nmd), true + } - "no-container": report.NodeMetadata{}, - }, - EdgeMetadatas: report.EdgeMetadatas{}, + mapper := render.Map{ + MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) { + return render.RenderableNode{ID: "_" + nodes.ID}, true }, - Address: report.Topology{ - Adjacency: report.Adjacency{ - report.MakeAdjacencyID(clientAddressNodeID): report.MakeIDList(serverAddressNodeID), - report.MakeAdjacencyID(randomAddressNodeID): report.MakeIDList(serverAddressNodeID), - report.MakeAdjacencyID(serverAddressNodeID): report.MakeIDList(clientAddressNodeID, unknownAddressNodeID), // no backlink to random - }, - NodeMetadatas: report.NodeMetadatas{ - clientAddressNodeID: report.NodeMetadata{ - "name": "client.hostname.com", // hostname - "host_name": "client.hostname.com", - report.HostNodeID: clientHostNodeID, - }, - randomAddressNodeID: report.NodeMetadata{ - "name": "random.hostname.com", // hostname - report.HostNodeID: randomHostNodeID, - }, - serverAddressNodeID: report.NodeMetadata{ - "name": "server.hostname.com", // hostname - report.HostNodeID: serverHostNodeID, - }, - }, - EdgeMetadatas: report.EdgeMetadatas{ - report.MakeEdgeID(clientAddressNodeID, serverAddressNodeID): report.EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 3, - }, - report.MakeEdgeID(randomAddressNodeID, serverAddressNodeID): report.EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 20, // dangling connections, weird but possible - }, - report.MakeEdgeID(serverAddressNodeID, clientAddressNodeID): report.EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 3, - }, - report.MakeEdgeID(serverAddressNodeID, unknownAddressNodeID): report.EdgeMetadata{ - WithConnCountTCP: true, - MaxConnCountTCP: 7, - }, - }, - }, - Host: report.Topology{ - Adjacency: report.Adjacency{}, - NodeMetadatas: report.NodeMetadatas{ - serverHostNodeID: report.NodeMetadata{ - "host_name": serverHostName, - "local_networks": "10.10.10.0/24", - "os": "Linux", - "load": "0.01 0.01 0.01", - }, - }, - EdgeMetadatas: report.EdgeMetadatas{}, + Renderer: render.LeafMap{ + Selector: selector, + Mapper: identity, + Pseudo: nil, }, } -) -func TestRenderByEndpointPID(t *testing.T) { - want := render.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{}, - }, + want := report.AggregateMetadata{ + report.KeyBytesIngress: 1, + report.KeyBytesEgress: 2, } - have := render.Topology(rpt.Endpoint, render.ProcessPID, render.GenericPseudoNode) + have := mapper.AggregateMetadata(report.MakeReport(), "_foo", "_bar") if !reflect.DeepEqual(want, have) { - t.Error("\n" + diff(want, have)) + t.Errorf("want %+v, have %+v", 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. +func TestFilterRender(t *testing.T) { + renderer := render.FilterUnconnected{ + Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ + "foo": {ID: "foo", Adjacency: report.MakeIDList("bar")}, + "bar": {ID: "bar", Adjacency: report.MakeIDList("foo")}, + "baz": {ID: "baz", Adjacency: report.MakeIDList()}, + }}, + } want := render.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{}, - }, + "foo": {ID: "foo", Adjacency: report.MakeIDList("bar")}, + "bar": {ID: "bar", Adjacency: report.MakeIDList("foo")}, } - have := render.Topology(rpt.Endpoint, render.ProcessName, render.GenericGroupedPseudoNode) + have := renderer.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { - t.Error("\n" + diff(want, have)) + t.Errorf("want %+v, have %+v", want, have) } } - -func TestRenderByNetworkHostname(t *testing.T) { - want := render.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 := render.Topology(rpt.Address, render.NetworkHostname, render.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/render/renderable_node.go b/render/renderable_node.go index 2b6fd9a03..e34db414a 100644 --- a/render/renderable_node.go +++ b/render/renderable_node.go @@ -8,14 +8,16 @@ import ( // an element of a topology. It should contain information that's relevant // to rendering a node when there are many nodes visible at once. type RenderableNode struct { - ID string `json:"id"` // - LabelMajor string `json:"label_major"` // e.g. "process", human-readable - LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional - Rank string `json:"rank"` // to help the layout engine - Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes - Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain) - Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information - Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums + ID string `json:"id"` // + LabelMajor string `json:"label_major"` // e.g. "process", human-readable + LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional + Rank string `json:"rank"` // to help the layout engine + Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes + Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain) + Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information + + report.AggregateMetadata `json:"metadata"` // Numeric sums + report.NodeMetadata `json:"-"` // merged NodeMetadata of the nodes used to build this } // RenderableNodes is a set of RenderableNodes @@ -54,5 +56,57 @@ func (rn *RenderableNode) Merge(other RenderableNode) { rn.Adjacency = rn.Adjacency.Add(other.Adjacency...) rn.Origins = rn.Origins.Add(other.Origins...) - rn.Metadata.Merge(other.Metadata) + rn.AggregateMetadata.Merge(other.AggregateMetadata) + rn.NodeMetadata.Merge(other.NodeMetadata) +} + +// NewRenderableNode makes a new RenderableNode +func NewRenderableNode(id, major, minor, rank string, nmd report.NodeMetadata) RenderableNode { + return RenderableNode{ + ID: id, + LabelMajor: major, + LabelMinor: minor, + Rank: rank, + Pseudo: false, + AggregateMetadata: report.AggregateMetadata{}, + NodeMetadata: nmd.Copy(), + } +} + +func newDerivedNode(id string, node RenderableNode) RenderableNode { + return RenderableNode{ + ID: id, + LabelMajor: "", + LabelMinor: "", + Rank: "", + Pseudo: node.Pseudo, + AggregateMetadata: node.AggregateMetadata, + Origins: node.Origins, + NodeMetadata: report.NodeMetadata{}, + } +} + +func newPseudoNode(id, major, minor string) RenderableNode { + return RenderableNode{ + ID: id, + LabelMajor: major, + LabelMinor: minor, + Rank: "", + Pseudo: true, + AggregateMetadata: report.AggregateMetadata{}, + NodeMetadata: report.NodeMetadata{}, + } +} + +func newDerivedPseudoNode(id, major string, node RenderableNode) RenderableNode { + return RenderableNode{ + ID: id, + LabelMajor: major, + LabelMinor: "", + Rank: "", + Pseudo: true, + AggregateMetadata: node.AggregateMetadata, + Origins: node.Origins, + NodeMetadata: report.NodeMetadata{}, + } } diff --git a/render/topologies.go b/render/topologies.go new file mode 100644 index 000000000..d8490e7d7 --- /dev/null +++ b/render/topologies.go @@ -0,0 +1,47 @@ +package render + +import ( + "github.com/weaveworks/scope/report" +) + +// EndpointRenderer is a Renderer which produces a renderable endpoint graph. +var EndpointRenderer = LeafMap{ + Selector: report.SelectEndpoint, + Mapper: MapEndpointIdentity, + Pseudo: GenericPseudoNode, +} + +// ProcessRenderer is a Renderer which produces a renderable process +// graph by merging the endpoint graph and the process topology. +var ProcessRenderer = MakeReduce( + Map{ + MapFunc: MapEndpoint2Process, + Renderer: EndpointRenderer, + }, + LeafMap{ + Selector: report.SelectProcess, + Mapper: MapProcessIdentity, + Pseudo: GenericPseudoNode, + }, +) + +// ProcessRenderer is a Renderer which produces a renderable process +// name graph by munging the progess graph. +var ProcessNameRenderer = Map{ + MapFunc: MapProcess2Name, + Renderer: ProcessRenderer, +} + +// ContainerRenderer is a Renderer which produces a renderable container +// graph by merging the process graph and the container topology. +var ContainerRenderer = MakeReduce( + Map{ + MapFunc: MapProcess2Container, + Renderer: ProcessRenderer, + }, + LeafMap{ + Selector: report.SelectContainer, + Mapper: MapContainerIdentity, + Pseudo: GenericPseudoNode, + }, +) diff --git a/render/topologies_test.go b/render/topologies_test.go new file mode 100644 index 000000000..0188c809f --- /dev/null +++ b/render/topologies_test.go @@ -0,0 +1,490 @@ +package render_test + +import ( + "fmt" + "reflect" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" + + "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" +) + +func init() { + spew.Config.SortKeys = true // :\ +} + +var ( + clientHostID = "client.hostname.com" + serverHostID = "server.hostname.com" + randomHostID = "random.hostname.com" + unknownHostID = "" + + clientIP = "10.10.10.20" + serverIP = "192.168.1.1" + clientPort54001 = "54001" + clientPort54002 = "54002" + serverPort = "80" + + clientHostName = clientHostID + serverHostName = serverHostID + + clientPID = "10001" + serverPID = "215" + nonContainerPID = "1234" + + clientHostNodeID = report.MakeHostNodeID(clientHostID) + serverHostNodeID = report.MakeHostNodeID(serverHostID) + randomHostNodeID = report.MakeHostNodeID(randomHostID) + + client54001NodeID = report.MakeEndpointNodeID(clientHostID, clientIP, clientPort54001) // curl (1) + client54002NodeID = report.MakeEndpointNodeID(clientHostID, clientIP, clientPort54002) // curl (2) + unknownClient1NodeID = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54010") // we want to ensure two unknown clients, connnected + unknownClient2NodeID = report.MakeEndpointNodeID(serverHostID, "10.10.10.10", "54020") // to the same server, are deduped. + unknownClient3NodeID = report.MakeEndpointNodeID(serverHostID, "10.10.10.11", "54020") // Check this one isn't deduped + server80NodeID = report.MakeEndpointNodeID(serverHostID, serverIP, serverPort) // apache + + clientAddressNodeID = report.MakeAddressNodeID(clientHostID, "10.10.10.20") + serverAddressNodeID = report.MakeAddressNodeID(serverHostID, "192.168.1.1") + randomAddressNodeID = report.MakeAddressNodeID(randomHostID, "172.16.11.9") // only in Address topology + unknownAddressNodeID = report.MakeAddressNodeID(unknownHostID, "10.10.10.10") + + clientProcessNodeID = report.MakeProcessNodeID(clientHostID, clientPID) + serverProcessNodeID = report.MakeProcessNodeID(serverHostID, serverPID) + nonContainerProcessNodeID = report.MakeProcessNodeID(serverHostID, nonContainerPID) + + clientContainerID = "a1b2c3d4e5" + serverContainerID = "5e4d3c2b1a" + clientContainerNodeID = report.MakeContainerNodeID(clientHostID, clientContainerID) + serverContainerNodeID = report.MakeContainerNodeID(serverHostID, serverContainerID) +) + +var ( + rpt = 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), + }, + 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 :) + client54001NodeID: report.NodeMetadata{ + "addr": clientIP, + "port": clientPort54001, + "pid": clientPID, + report.HostNodeID: clientHostNodeID, + }, + client54002NodeID: report.NodeMetadata{ + "addr": clientIP, + "port": clientPort54002, + "pid": clientPID, // should be same as above! + report.HostNodeID: clientHostNodeID, + }, + server80NodeID: report.NodeMetadata{ + "addr": serverIP, + "port": serverPort, + "pid": serverPID, + report.HostNodeID: serverHostNodeID, + }, + }, + EdgeMetadatas: report.EdgeMetadatas{ + report.MakeEdgeID(client54001NodeID, server80NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 100, + BytesEgress: 10, + }, + report.MakeEdgeID(client54002NodeID, server80NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 200, + BytesEgress: 20, + }, + + report.MakeEdgeID(server80NodeID, client54001NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 10, + BytesEgress: 100, + }, + report.MakeEdgeID(server80NodeID, client54002NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 20, + BytesEgress: 200, + }, + report.MakeEdgeID(server80NodeID, unknownClient1NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 30, + BytesEgress: 300, + }, + report.MakeEdgeID(server80NodeID, unknownClient2NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 40, + BytesEgress: 400, + }, + report.MakeEdgeID(server80NodeID, unknownClient3NodeID): report.EdgeMetadata{ + WithBytes: true, + BytesIngress: 50, + BytesEgress: 500, + }, + }, + }, + Process: report.Topology{ + Adjacency: report.Adjacency{}, + NodeMetadatas: report.NodeMetadatas{ + clientProcessNodeID: report.NodeMetadata{ + "pid": clientPID, + "comm": "curl", + "docker_container_id": clientContainerID, + report.HostNodeID: clientHostNodeID, + }, + serverProcessNodeID: report.NodeMetadata{ + "pid": serverPID, + "comm": "apache", + "docker_container_id": serverContainerID, + report.HostNodeID: serverHostNodeID, + }, + nonContainerProcessNodeID: report.NodeMetadata{ + "pid": nonContainerPID, + "comm": "bash", + report.HostNodeID: serverHostNodeID, + }, + }, + EdgeMetadatas: report.EdgeMetadatas{}, + }, + Container: report.Topology{ + NodeMetadatas: report.NodeMetadatas{ + clientContainerNodeID: report.NodeMetadata{ + "docker_container_id": clientContainerID, + "docker_container_name": "client", + report.HostNodeID: clientHostNodeID, + }, + serverContainerNodeID: report.NodeMetadata{ + "docker_container_id": serverContainerID, + "docker_container_name": "server", + report.HostNodeID: serverHostNodeID, + }, + }, + }, + Address: report.Topology{ + Adjacency: report.Adjacency{ + report.MakeAdjacencyID(clientAddressNodeID): report.MakeIDList(serverAddressNodeID), + report.MakeAdjacencyID(randomAddressNodeID): report.MakeIDList(serverAddressNodeID), + report.MakeAdjacencyID(serverAddressNodeID): report.MakeIDList(clientAddressNodeID, unknownAddressNodeID), // no backlink to random + }, + NodeMetadatas: report.NodeMetadatas{ + clientAddressNodeID: report.NodeMetadata{ + "name": "client.hostname.com", // hostname + "host_name": "client.hostname.com", + report.HostNodeID: clientHostNodeID, + }, + randomAddressNodeID: report.NodeMetadata{ + "name": "random.hostname.com", // hostname + report.HostNodeID: randomHostNodeID, + }, + serverAddressNodeID: report.NodeMetadata{ + "name": "server.hostname.com", // hostname + report.HostNodeID: serverHostNodeID, + }, + }, + EdgeMetadatas: report.EdgeMetadatas{ + report.MakeEdgeID(clientAddressNodeID, serverAddressNodeID): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 3, + }, + report.MakeEdgeID(randomAddressNodeID, serverAddressNodeID): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 20, // dangling connections, weird but possible + }, + report.MakeEdgeID(serverAddressNodeID, clientAddressNodeID): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 3, + }, + report.MakeEdgeID(serverAddressNodeID, unknownAddressNodeID): report.EdgeMetadata{ + WithConnCountTCP: true, + MaxConnCountTCP: 7, + }, + }, + }, + Host: report.Topology{ + Adjacency: report.Adjacency{}, + NodeMetadatas: report.NodeMetadatas{ + serverHostNodeID: report.NodeMetadata{ + "host_name": serverHostName, + "local_networks": "10.10.10.0/24", + "os": "Linux", + "load": "0.01 0.01 0.01", + }, + }, + EdgeMetadatas: report.EdgeMetadatas{}, + }, + } +) + +func init() { + if err := rpt.Validate(); err != nil { + panic(err) + } +} + +func trimNodeMetadata(rns render.RenderableNodes) render.RenderableNodes { + result := render.RenderableNodes{} + for id, rn := range rns { + rn.NodeMetadata = nil + result[id] = rn + } + return result +} + +func TestProcessRenderer(t *testing.T) { + var ( + clientProcessID = fmt.Sprintf("pid:%s:%s", clientHostID, clientPID) + serverProcessID = fmt.Sprintf("pid:%s:%s", serverHostID, serverPID) + nonContainerProcessID = fmt.Sprintf("pid:%s:%s", serverHostID, nonContainerPID) + ) + + want := render.RenderableNodes{ + clientProcessID: { + ID: clientProcessID, + LabelMajor: "curl", + LabelMinor: fmt.Sprintf("%s (%s)", clientHostID, clientPID), + Rank: clientPID, + Pseudo: false, + Adjacency: report.MakeIDList(serverProcessID), + Origins: report.MakeIDList(client54001NodeID, client54002NodeID, clientProcessNodeID, clientHostNodeID), + AggregateMetadata: report.AggregateMetadata{ + report.KeyBytesIngress: 300, + report.KeyBytesEgress: 30, + }, + }, + serverProcessID: { + ID: serverProcessID, + LabelMajor: "apache", + LabelMinor: fmt.Sprintf("%s (%s)", serverHostID, serverPID), + Rank: serverPID, + Pseudo: false, + Adjacency: report.MakeIDList( + clientProcessID, + "pseudo;10.10.10.10;192.168.1.1;80", + "pseudo;10.10.10.11;192.168.1.1;80", + ), + Origins: report.MakeIDList(server80NodeID, serverProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{ + report.KeyBytesIngress: 150, + report.KeyBytesEgress: 1500, + }, + }, + nonContainerProcessID: { + ID: nonContainerProcessID, + LabelMajor: "bash", + LabelMinor: fmt.Sprintf("%s (%s)", serverHostID, nonContainerPID), + Rank: nonContainerPID, + Pseudo: false, + Adjacency: report.MakeIDList(), + Origins: report.MakeIDList(nonContainerProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{}, + }, + "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, + AggregateMetadata: 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, + AggregateMetadata: report.AggregateMetadata{}, + }, + } + have := render.ProcessRenderer.Render(rpt) + have = trimNodeMetadata(have) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func TestProcessNameRenderer(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 := render.RenderableNodes{ + "curl": { + ID: "curl", + LabelMajor: "curl", + LabelMinor: "", + Rank: "curl", + Pseudo: false, + Adjacency: report.MakeIDList("apache"), + Origins: report.MakeIDList(client54001NodeID, client54002NodeID, clientProcessNodeID, clientHostNodeID), + AggregateMetadata: 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;192.168.1.1;80", + "pseudo;10.10.10.11;192.168.1.1;80", + ), + Origins: report.MakeIDList(server80NodeID, serverProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{ + report.KeyBytesIngress: 150, + report.KeyBytesEgress: 1500, + }, + }, + "bash": { + ID: "bash", + LabelMajor: "bash", + LabelMinor: "", + Rank: "bash", + Pseudo: false, + Origins: report.MakeIDList(nonContainerProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{}, + }, + "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, + AggregateMetadata: 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, + AggregateMetadata: report.AggregateMetadata{}, + }, + } + have := render.ProcessNameRenderer.Render(rpt) + have = trimNodeMetadata(have) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func TestContainerRenderer(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 := render.RenderableNodes{ + clientContainerID: { + ID: clientContainerID, + LabelMajor: "client", + LabelMinor: clientHostName, + Rank: "", + Pseudo: false, + Adjacency: report.MakeIDList(serverContainerID), + Origins: report.MakeIDList(clientContainerNodeID, client54001NodeID, client54002NodeID, clientProcessNodeID, clientHostNodeID), + AggregateMetadata: report.AggregateMetadata{ + report.KeyBytesIngress: 300, + report.KeyBytesEgress: 30, + }, + }, + serverContainerID: { + ID: serverContainerID, + LabelMajor: "server", + LabelMinor: serverHostName, + Rank: "", + Pseudo: false, + Adjacency: report.MakeIDList(clientContainerID, render.UncontainedID), + Origins: report.MakeIDList(serverContainerNodeID, server80NodeID, serverProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{ + report.KeyBytesIngress: 150, + report.KeyBytesEgress: 1500, + }, + }, + render.UncontainedID: { + ID: render.UncontainedID, + LabelMajor: render.UncontainedMajor, + LabelMinor: "", + Rank: "", + Pseudo: true, + Origins: report.MakeIDList(nonContainerProcessNodeID, serverHostNodeID), + AggregateMetadata: report.AggregateMetadata{}, + }, + } + have := render.ContainerRenderer.Render(rpt) + have = trimNodeMetadata(have) + if !reflect.DeepEqual(want, have) { + t.Error("\n" + diff(want, have)) + } +} + +func TestRenderByNetworkHostname(t *testing.T) { + want := render.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")), + AggregateMetadata: 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")), + AggregateMetadata: 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")), + AggregateMetadata: 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, + AggregateMetadata: report.AggregateMetadata{}, + }, + } + have := render.LeafMap{ + Selector: report.SelectAddress, + Mapper: render.NetworkHostname, + Pseudo: render.GenericPseudoNode, + }.Render(rpt) + have = trimNodeMetadata(have) + 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/id.go b/report/id.go index df7c300e1..f29a91905 100644 --- a/report/id.go +++ b/report/id.go @@ -97,6 +97,12 @@ func ParseNodeID(nodeID string) (hostID string, remainder string, ok bool) { return fields[0], fields[1], true } +// ExtractHostID extracts the host id from NodeMetadata +func ExtractHostID(m NodeMetadata) string { + hostid, _, _ := ParseNodeID(m[HostNodeID]) + return hostid +} + // MakePseudoNodeID produces a pseudo node ID from its composite parts. func MakePseudoNodeID(parts ...string) string { return strings.Join(append([]string{"pseudo"}, parts...), ScopeDelim) diff --git a/report/report.go b/report/report.go index 8582469e6..fccb697e9 100644 --- a/report/report.go +++ b/report/report.go @@ -53,6 +53,11 @@ func SelectEndpoint(r Report) Topology { return r.Endpoint } +// SelectProcess selects the process topology. +func SelectProcess(r Report) Topology { + return r.Process +} + // SelectAddress selects the address topology. func SelectAddress(r Report) Topology { return r.Address