Merge pull request #248 from tomwilkie/228-dev

Produce containers topology from endpoints via processes
This commit is contained in:
Tom Wilkie
2015-06-17 20:28:05 +02:00
20 changed files with 1119 additions and 569 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)

View File

@@ -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")),

View File

@@ -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},
},
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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: ""})

View File

@@ -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))
}
}

View File

@@ -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.

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{},
}
}

47
render/topologies.go Normal file
View File

@@ -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,
},
)

490
render/topologies_test.go Normal file
View File

@@ -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
}

View File

@@ -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)

View File

@@ -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