diff --git a/Makefile b/Makefile index 5262b44806..6b8ff6568d 100644 --- a/Makefile +++ b/Makefile @@ -154,6 +154,9 @@ clean: $(SCOPE_EXE) $(RUNSVINIT) prog/static.go client/build/*.js docker/weave .pkg \ $(CODECGEN_TARGETS) $(CODECGEN_DIR)/bin +clean-codecgen: + rm -rf $(CODECGEN_TARGETS) $(CODECGEN_DIR)/bin + deps: $(GO) get -u -f -tags $(GO_BUILD_TAGS) \ github.com/FiloSottile/gvt \ diff --git a/app/api_topologies.go b/app/api_topologies.go index a76c851da7..71aa66ca73 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -266,7 +266,7 @@ func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStat ) for _, n := range renderer.Render(rpt) { nodes++ - if !n.Pseudo { + if n.Topology != render.Pseudo { realNodes++ } edges += len(n.Adjacency) diff --git a/app/api_topology.go b/app/api_topology.go index e28df96193..712b6d7f4a 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -18,7 +18,7 @@ const ( // APITopology is returned by the /api/topology/{name} handler. type APITopology struct { - Nodes render.RenderableNodes `json:"nodes"` + Nodes detailed.NodeSummaries `json:"nodes"` } // APINode is returned by the /api/topology/{name}/{id} handler. @@ -34,7 +34,7 @@ func handleTopology(ctx context.Context, rep Reporter, renderer render.Renderer, return } respondWith(w, http.StatusOK, APITopology{ - Nodes: renderer.Render(report).Prune(), + Nodes: detailed.Summaries(renderer.Render(report)), }) } @@ -104,7 +104,7 @@ func handleWebsocket( }(conn) var ( - previousTopo render.RenderableNodes + previousTopo detailed.NodeSummaries tick = time.Tick(loop) wait = make(chan struct{}, 1) ) @@ -117,8 +117,8 @@ func handleWebsocket( log.Errorf("Error generating report: %v", err) return } - newTopo := renderer.Render(report).Prune() - diff := render.TopoDiff(previousTopo, newTopo) + newTopo := detailed.Summaries(renderer.Render(report)) + diff := detailed.TopoDiff(previousTopo, newTopo) previousTopo = newTopo if err := conn.WriteJSON(diff); err != nil { diff --git a/app/api_topology_test.go b/app/api_topology_test.go index 7c3ae88c02..aacbea59e4 100644 --- a/app/api_topology_test.go +++ b/app/api_topology_test.go @@ -3,16 +3,14 @@ package app_test import ( "fmt" "net/url" - "reflect" "testing" "github.com/gorilla/websocket" "github.com/ugorji/go/codec" "github.com/weaveworks/scope/app" - "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/render/detailed" "github.com/weaveworks/scope/render/expected" - "github.com/weaveworks/scope/test" "github.com/weaveworks/scope/test/fixture" ) @@ -59,13 +57,13 @@ func TestAPITopologyProcesses(t *testing.T) { defer ts.Close() is404(t, ts, "/api/topology/processes/foobar") { - body := getRawJSON(t, ts, "/api/topology/processes/"+expected.ServerProcessID) + body := getRawJSON(t, ts, "/api/topology/processes/"+fixture.ServerProcessNodeID) var node app.APINode decoder := codec.NewDecoderBytes(body, &codec.JsonHandle{}) if err := decoder.Decode(&node); err != nil { t.Fatal(err) } - equals(t, expected.ServerProcessID, node.Node.ID) + equals(t, fixture.ServerProcessNodeID, node.Node.ID) equals(t, "apache", node.Node.Label) equals(t, false, node.Node.Pseudo) // Let's not unit-test the specific content of the detail tables @@ -98,18 +96,21 @@ func TestAPITopologyHosts(t *testing.T) { t.Fatal(err) } - if want, have := expected.RenderedHosts, topo.Nodes.Prune(); !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + // Should have the rendered host nodes + for id := range expected.RenderedHosts { + if _, ok := topo.Nodes[id]; !ok { + t.Errorf("Expected output to include node: %s, but wasn't found", id) + } } } { - body := getRawJSON(t, ts, "/api/topology/hosts/"+expected.ServerHostID) + body := getRawJSON(t, ts, "/api/topology/hosts/"+fixture.ServerHostNodeID) var node app.APINode decoder := codec.NewDecoderBytes(body, &codec.JsonHandle{}) if err := decoder.Decode(&node); err != nil { t.Fatal(err) } - equals(t, expected.ServerHostID, node.Node.ID) + equals(t, fixture.ServerHostNodeID, node.Node.ID) equals(t, "server", node.Node.Label) equals(t, false, node.Node.Pseudo) // Let's not unit-test the specific content of the detail tables @@ -141,7 +142,7 @@ func TestAPITopologyWebsocket(t *testing.T) { _, p, err := ws.ReadMessage() ok(t, err) - var d render.Diff + var d detailed.Diff decoder := codec.NewDecoderBytes(p, &codec.JsonHandle{}) if err := decoder.Decode(&d); err != nil { t.Fatalf("JSON parse error: %s", err) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 8e70a45a0f..ee131c6632 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -244,7 +244,7 @@ export default class NodesChart extends React.Component { // copy relevant fields to state nodes return topology.map((node, id) => makeMap({ id, - label: node.get('label_major'), + label: node.get('label'), pseudo: node.get('pseudo'), subLabel: node.get('label_minor'), nodeCount: node.get('node_count'), diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js index 8acfa6023a..bdb545ec54 100644 --- a/client/app/scripts/components/debug-toolbar.js +++ b/client/app/scripts/components/debug-toolbar.js @@ -21,7 +21,7 @@ const deltaAdd = (name, adjacency = [], shape = 'circle', stack = false, nodeCou stack, node_count: nodeCount, id: name, - label_major: name, + label: name, label_minor: 'weave-1', latest: {}, metadata: {}, diff --git a/client/app/scripts/components/embedded-terminal.js b/client/app/scripts/components/embedded-terminal.js index 291820fb00..b80909b5bf 100644 --- a/client/app/scripts/components/embedded-terminal.js +++ b/client/app/scripts/components/embedded-terminal.js @@ -8,9 +8,9 @@ import { DETAILS_PANEL_WIDTH, DETAILS_PANEL_MARGINS, export default function EmeddedTerminal({pipe, nodeId, details}) { const node = details.get(nodeId); const d = node && node.details; - const titleBarColor = d && getNodeColorDark(d.rank, d.label_major); - const statusBarColor = d && getNodeColor(d.rank, d.label_major); - const title = d && d.label_major; + const titleBarColor = d && getNodeColorDark(d.rank, d.label); + const statusBarColor = d && getNodeColor(d.rank, d.label); + const title = d && d.label; const style = { right: DETAILS_PANEL_MARGINS.right + DETAILS_PANEL_WIDTH + 10 + diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index a8d93c4e34..6fc44676db 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -54,7 +54,7 @@ export default class NodeDetails extends React.Component { renderLoading() { const node = this.props.nodes.get(this.props.nodeId); - const label = node ? node.get('label_major') : this.props.label; + const label = node ? node.get('label') : this.props.label; const nodeColor = (node ? getNodeColorDark(node.get('rank'), label, node.get('pseudo')) : getNeutralColor()); diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js index 03c1670049..cde7bd819c 100644 --- a/client/app/scripts/components/sparkline.js +++ b/client/app/scripts/components/sparkline.js @@ -20,7 +20,7 @@ export default class Sparkline extends React.Component { let data = this.props.data; // Do nothing if no data or data w/o date are passed in. - if (data.length === 0 || data[0].date === undefined) { + if (data === undefined || data.length === 0 || data[0].date === undefined) { return
; } diff --git a/client/app/scripts/stores/__tests__/app-store-test.js b/client/app/scripts/stores/__tests__/app-store-test.js index 5ccee481b9..379e878e9d 100644 --- a/client/app/scripts/stores/__tests__/app-store-test.js +++ b/client/app/scripts/stores/__tests__/app-store-test.js @@ -17,7 +17,7 @@ describe('AppStore', () => { rank: undefined, adjacency: ['n1', 'n2'], pseudo: undefined, - label_major: undefined, + label: undefined, label_minor: undefined }, n2: { @@ -25,7 +25,7 @@ describe('AppStore', () => { rank: undefined, adjacency: undefined, pseudo: undefined, - label_major: undefined, + label: undefined, label_minor: undefined } }; diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 66abc2c2a6..d6b2668845 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -22,7 +22,7 @@ const error = debug('scope:error'); function makeNode(node) { return { id: node.id, - label_major: node.label_major, + label: node.label, label_minor: node.label_minor, node_count: node.node_count, rank: node.rank, diff --git a/experimental/_integration/easy_test.go b/experimental/_integration/easy_test.go index 08e2260e06..9690847a7b 100644 --- a/experimental/_integration/easy_test.go +++ b/experimental/_integration/easy_test.go @@ -61,9 +61,9 @@ func TestMultipleProbes(t *testing.T) { }) } -func parseTopology(t *testing.T, p []byte) map[string]report.RenderableNode { +func parseTopology(t *testing.T, p []byte) map[string]report.Node { var r struct { - Nodes map[string]report.RenderableNode `json:"nodes"` + Nodes map[string]report.Node `json:"nodes"` } if err := json.Unmarshal(p, &r); err != nil { @@ -85,7 +85,7 @@ func parseEdge(t *testing.T, p []byte) map[string]interface{} { return edge.Metadata } -func assertAdjacent(t *testing.T, n report.RenderableNode, ids ...string) { +func assertAdjacent(t *testing.T, n report.Node, ids ...string) { want := report.MakeIDList(ids...) if have := n.Adjacency; !reflect.DeepEqual(want, have) { diff --git a/experimental/graphviz/handlers.go b/experimental/graphviz/handlers.go index e7e9b97b08..307c55c032 100644 --- a/experimental/graphviz/handlers.go +++ b/experimental/graphviz/handlers.go @@ -10,18 +10,18 @@ import ( "os" "os/exec" - "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/render/detailed" "github.com/weaveworks/scope/report" ) -func dot(w io.Writer, t render.RenderableNodes) { +func dot(w io.Writer, t detailed.NodeSummaries) { fmt.Fprintf(w, "digraph G {\n") fmt.Fprintf(w, "\toutputorder=edgesfirst;\n") fmt.Fprintf(w, "\toverlap=scale;\n") fmt.Fprintf(w, "\tnode [style=filled];\n") fmt.Fprintf(w, "\t\n") for id, rn := range t { - label := rn.LabelMajor + label := rn.Label if len(label) > 20 { label = label[:20] + "..." } diff --git a/experimental/graphviz/render.go b/experimental/graphviz/render.go index ad36b3c723..de452b89fa 100644 --- a/experimental/graphviz/render.go +++ b/experimental/graphviz/render.go @@ -4,10 +4,11 @@ import ( "fmt" "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/render/detailed" "github.com/weaveworks/scope/report" ) -func renderTo(rpt report.Report, topology string) (render.RenderableNodes, error) { +func renderTo(rpt report.Report, topology string) (detailed.NodeSummaries, error) { renderer, ok := map[string]render.Renderer{ "processes": render.FilterUnconnected(render.ProcessWithContainerNameRenderer), "processes-by-name": render.FilterUnconnected(render.ProcessNameRenderer), @@ -16,7 +17,7 @@ func renderTo(rpt report.Report, topology string) (render.RenderableNodes, error "hosts": render.HostRenderer, }[topology] if !ok { - return render.RenderableNodes{}, fmt.Errorf("unknown topology %v", topology) + return detailed.NodeSummaries{}, fmt.Errorf("unknown topology %v", topology) } - return renderer.Render(rpt), nil + return detailed.Summaries(renderer.Render(rpt)), nil } diff --git a/integration/config.sh b/integration/config.sh index 251c436f10..a240aa3470 100644 --- a/integration/config.sh +++ b/integration/config.sh @@ -39,7 +39,7 @@ has() { local host=$2 local name=$3 local count=${4:-1} - assert "curl -s http://${host}:4040/api/topology/${view}?system=show | jq -r '[.nodes[] | select(.label_major == \"${name}\")] | length'" $count + assert "curl -s http://${host}:4040/api/topology/${view}?system=show | jq -r '[.nodes[] | select(.label == \"${name}\")] | length'" $count } # this checks we have a named container @@ -51,7 +51,7 @@ node_id() { local view="$1" local host="$2" local name="$3" - echo $(curl -s http://${host}:4040/api/topology/${view}?system=show | jq -r ".nodes[] | select(.label_major == \"${name}\") | .id") + echo $(curl -s http://${host}:4040/api/topology/${view}?system=show | jq -r ".nodes[] | select(.label == \"${name}\") | .id") } container_id() { @@ -103,7 +103,7 @@ wait_for() { local nodes="$(curl -s http://$host:4040/api/topology/${view}?system=show)" local found=0 for name in "$@"; do - local count=$(echo "${nodes}" | jq -r "[.nodes[] | select(.label_major == \"${name}\")] | length") + local count=$(echo "${nodes}" | jq -r "[.nodes[] | select(.label == \"${name}\")] | length") if [ -n "${count}" ] && [ "${count}" -ge 1 ]; then found=$(( found + 1 )) fi diff --git a/render/benchmark_test.go b/render/benchmark_test.go index 84afbb7589..05889b21ed 100644 --- a/render/benchmark_test.go +++ b/render/benchmark_test.go @@ -14,7 +14,7 @@ import ( var ( benchReportFile = flag.String("bench-report-file", "", "json report file to use for benchmarking (relative to this package)") - benchmarkRenderResult map[string]render.RenderableNode + benchmarkRenderResult map[string]report.Node benchmarkStatsResult render.Stats ) diff --git a/render/detailed/connections.go b/render/detailed/connections.go index 1c48dd6d07..ca446d0977 100644 --- a/render/detailed/connections.go +++ b/render/detailed/connections.go @@ -30,12 +30,12 @@ var ( ) type connectionsRow struct { - remoteNode, localNode *render.RenderableNode + remoteNode, localNode *report.Node remoteAddr, localAddr string port string // always the server-side port } -func incomingConnectionsTable(topologyID string, n render.RenderableNode, ns render.RenderableNodes) NodeSummaryGroup { +func incomingConnectionsTable(topologyID string, n report.Node, ns report.Nodes) NodeSummaryGroup { localEndpointIDs := endpointChildIDsOf(n) // For each node which has an edge TO me @@ -54,7 +54,7 @@ func incomingConnectionsTable(topologyID string, n render.RenderableNode, ns ren // details on the internet node) for _, child := range endpointChildrenOf(node) { for _, localEndpointID := range child.Adjacency.Intersection(localEndpointIDs) { - _, localAddr, port, ok := render.ParseEndpointID(localEndpointID) + _, localAddr, port, ok := report.ParseEndpointNodeID(localEndpointID) if !ok { continue } @@ -84,26 +84,27 @@ func incomingConnectionsTable(topologyID string, n render.RenderableNode, ns ren } } -func outgoingConnectionsTable(topologyID string, n render.RenderableNode, ns render.RenderableNodes) NodeSummaryGroup { +func outgoingConnectionsTable(topologyID string, n report.Node, ns report.Nodes) NodeSummaryGroup { localEndpoints := endpointChildrenOf(n) // For each node which has an edge FROM me counts := map[connectionsRow]int{} - for _, node := range ns { - if !n.Adjacency.Contains(node.ID) { + for _, id := range n.Adjacency { + node, ok := ns[id] + if !ok { continue } remoteNode := node.Copy() remoteEndpointIDs := endpointChildIDsOf(remoteNode) for _, localEndpoint := range localEndpoints { - _, localAddr, _, ok := render.ParseEndpointID(localEndpoint.ID) + _, localAddr, _, ok := report.ParseEndpointNodeID(localEndpoint.ID) if !ok { continue } for _, remoteEndpointID := range localEndpoint.Adjacency.Intersection(remoteEndpointIDs) { - _, _, port, ok := render.ParseEndpointID(remoteEndpointID) + _, _, port, ok := report.ParseEndpointNodeID(remoteEndpointID) if !ok { continue } @@ -133,47 +134,54 @@ func outgoingConnectionsTable(topologyID string, n render.RenderableNode, ns ren } } -func endpointChildrenOf(n render.RenderableNode) []render.RenderableNode { - result := []render.RenderableNode{} - n.Children.ForEach(func(child render.RenderableNode) { - if _, _, _, ok := render.ParseEndpointID(child.ID); ok { +func endpointChildrenOf(n report.Node) []report.Node { + result := []report.Node{} + n.Children.ForEach(func(child report.Node) { + if _, _, _, ok := report.ParseEndpointNodeID(child.ID); ok { result = append(result, child) } }) return result } -func endpointChildIDsOf(n render.RenderableNode) report.IDList { +func endpointChildIDsOf(n report.Node) report.IDList { result := report.MakeIDList() - n.Children.ForEach(func(child render.RenderableNode) { - if _, _, _, ok := render.ParseEndpointID(child.ID); ok { - result = append(result, child.ID) + n.Children.ForEach(func(child report.Node) { + if _, _, _, ok := report.ParseEndpointNodeID(child.ID); ok { + result = result.Add(child.ID) } }) return result } -func isInternetNode(n render.RenderableNode) bool { +func isInternetNode(n report.Node) bool { return n.ID == render.IncomingInternetID || n.ID == render.OutgoingInternetID } func connectionRows(in map[connectionsRow]int, includeLocal bool) []NodeSummary { nodes := []NodeSummary{} for row, count := range in { - id, label, linkable := row.remoteNode.ID, row.remoteNode.LabelMajor, true - if row.remoteAddr != "" { - id, label, linkable = row.remoteAddr+":"+row.port, row.remoteAddr, false + // Use MakeNodeSummary to render the id and label of this node + // TODO(paulbellamy): Would be cleaner if we hade just a + // MakeNodeID(*row.remoteode). As we don't need the whole summary. + summary, ok := MakeNodeSummary(*row.remoteNode) + summary.Metadata, summary.Metrics, summary.DockerLabels = nil, nil, nil + if !ok && row.remoteAddr != "" { + summary = NodeSummary{ + ID: row.remoteAddr + ":" + row.port, + Label: row.remoteAddr, + Linkable: false, + } } - metadata := []MetadataRow{} if includeLocal { - metadata = append(metadata, + summary.Metadata = append(summary.Metadata, MetadataRow{ ID: "foo", Value: row.localAddr, Datatype: number, }) } - metadata = append(metadata, + summary.Metadata = append(summary.Metadata, MetadataRow{ ID: portKey, Value: row.port, @@ -185,12 +193,7 @@ func connectionRows(in map[connectionsRow]int, includeLocal bool) []NodeSummary Datatype: number, }, ) - nodes = append(nodes, NodeSummary{ - ID: id, - Label: label, - Linkable: linkable, - Metadata: metadata, - }) + nodes = append(nodes, summary) } sort.Sort(nodeSummariesByID(nodes)) return nodes diff --git a/render/detailed/docker_labels.go b/render/detailed/docker_labels.go index b8f886eccb..0e086e1c81 100644 --- a/render/detailed/docker_labels.go +++ b/render/detailed/docker_labels.go @@ -11,6 +11,11 @@ import ( // on an origin ID, which is (optimistically) a node ID in one of our // topologies. func NodeDockerLabels(nmd report.Node) []MetadataRow { + if _, ok := nmd.Counters.Lookup(nmd.Topology); ok { + // This is a group of nodes, so no docker labels! + return nil + } + if nmd.Topology != report.Container && nmd.Topology != report.ContainerImage { return nil } diff --git a/render/detailed/labels.go b/render/detailed/labels.go index 0a5fd4735b..556e459a02 100644 --- a/render/detailed/labels.go +++ b/render/detailed/labels.go @@ -8,7 +8,7 @@ import ( "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/overlay" "github.com/weaveworks/scope/probe/process" - "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" ) var labels = map[string]string{ @@ -46,7 +46,7 @@ var labels = map[string]string{ process.PID: "PID", process.PPID: "Parent PID", process.Threads: "# Threads", - render.ContainersKey: "# Containers", + report.Container: "# Containers", } // Label maps from the internal keys to the human-readable label for a piece diff --git a/render/detailed/metadata.go b/render/detailed/metadata.go index dba2912039..cce29789bc 100644 --- a/render/detailed/metadata.go +++ b/render/detailed/metadata.go @@ -11,7 +11,6 @@ import ( "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/overlay" "github.com/weaveworks/scope/probe/process" - "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) @@ -37,7 +36,7 @@ var ( } containerImageNodeMetadata = []MetadataRowTemplate{ Latest{ID: docker.ImageID, Truncate: 12, Prime: true}, - Counter{ID: render.ContainersKey, Prime: true}, + Counter{ID: report.Container, Prime: true}, } podNodeMetadata = []MetadataRowTemplate{ Latest{ID: kubernetes.PodID, Prime: true}, @@ -119,10 +118,7 @@ type MetadataRow struct { // Copy returns a value copy of a metadata row. func (m MetadataRow) Copy() MetadataRow { - return MetadataRow{ - ID: m.ID, - Value: m.Value, - } + return m } // MarshalJSON shouldn't be used, use CodecEncodeSelf instead @@ -171,6 +167,11 @@ func (m *MetadataRow) CodecDecodeSelf(decoder *codec.Decoder) { // NodeMetadata produces a table (to be consumed directly by the UI) based on // an origin ID, which is (optimistically) a node ID in one of our topologies. func NodeMetadata(n report.Node) []MetadataRow { + if _, ok := n.Counters.Lookup(n.Topology); ok { + // This is a group of nodes, so no metadata! + return nil + } + renderers := map[string][]MetadataRowTemplate{ report.Process: processNodeMetadata, report.Container: containerNodeMetadata, diff --git a/render/detailed/metadata_test.go b/render/detailed/metadata_test.go index 47feeba536..39ccc2658a 100644 --- a/render/detailed/metadata_test.go +++ b/render/detailed/metadata_test.go @@ -47,3 +47,29 @@ func TestNodeMetadata(t *testing.T) { } } } + +func TestMetadataRowCopy(t *testing.T) { + var ( + row = detailed.MetadataRow{ + ID: "id", + Value: "value", + Prime: true, + Datatype: "datatype", + } + cp = row.Copy() + ) + + // copy should be identical + if !reflect.DeepEqual(row, cp) { + t.Error(test.Diff(row, cp)) + } + + // changing the copy should not change the original + cp.ID = "" + cp.Value = "" + cp.Prime = false + cp.Datatype = "" + if row.ID != "id" || row.Value != "value" || row.Prime != true || row.Datatype != "datatype" { + t.Errorf("Expected changing the copy not to modify the original") + } +} diff --git a/render/detailed/metrics.go b/render/detailed/metrics.go index 5de036b156..5346dada21 100644 --- a/render/detailed/metrics.go +++ b/render/detailed/metrics.go @@ -47,6 +47,22 @@ type MetricRow struct { Metric *report.Metric } +// Summary returns a copy of the MetricRow, without the samples, just the value if there is one. +func (m MetricRow) Summary() MetricRow { + row := MetricRow{ + ID: m.ID, + Format: m.Format, + Group: m.Group, + Value: m.Value, + } + if m.Metric != nil { + var metric = m.Metric.Copy() + metric.Samples = nil + row.Metric = &metric + } + return row +} + // Copy returns a value copy of the MetricRow func (m MetricRow) Copy() MetricRow { row := MetricRow{ @@ -125,8 +141,13 @@ func (m *MetricRow) CodecDecodeSelf(decoder *codec.Decoder) { } // NodeMetrics produces a table (to be consumed directly by the UI) based on -// an origin ID, which is (optimistically) a node ID in one of our topologies. +// an a report.Node, which is (hopefully) a node in one of our topologies. func NodeMetrics(n report.Node) []MetricRow { + if _, ok := n.Counters.Lookup(n.Topology); ok { + // This is a group of nodes, so no metrics! + return nil + } + renderers := map[string][]MetricRow{ report.Process: processNodeMetrics, report.Container: containerNodeMetrics, diff --git a/render/detailed/metrics_test.go b/render/detailed/metrics_test.go index 3310a03981..d5085d0907 100644 --- a/render/detailed/metrics_test.go +++ b/render/detailed/metrics_test.go @@ -3,6 +3,7 @@ package detailed_test import ( "reflect" "testing" + "time" "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" @@ -110,3 +111,30 @@ func TestNodeMetrics(t *testing.T) { } } } + +func TestMetricRowSummary(t *testing.T) { + var ( + now = time.Now() + metric = report.MakeMetric().Add(now, 1.234) + row = detailed.MetricRow{ + ID: "id", + Format: "format", + Group: "group", + Value: 1.234, + Metric: &metric, + } + summary = row.Summary() + ) + // summary should have all the same fields + if row.ID != summary.ID || row.Format != summary.Format || row.Group != summary.Group || row.Value != summary.Value { + t.Errorf("Expected summary to have same fields as original: %#v, but had %#v", row, summary) + } + // summary should not have any samples + if summary.Metric.Len() != 0 { + t.Errorf("Expected summary to have no samples, but had %d", summary.Metric.Len()) + } + // original metric should still have its samples + if metric.Len() != 1 { + t.Errorf("Expected original metric to still have it's samples, but had %d", metric.Len()) + } +} diff --git a/render/detailed/node.go b/render/detailed/node.go index ab2831bdc6..8897f3ea93 100644 --- a/render/detailed/node.go +++ b/render/detailed/node.go @@ -8,7 +8,6 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/process" - "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) @@ -16,8 +15,6 @@ import ( // we want deep information about an individual node. type Node struct { NodeSummary - Rank string `json:"rank,omitempty"` - Pseudo bool `json:"pseudo,omitempty"` Controls []ControlInstance `json:"controls"` Children []NodeSummaryGroup `json:"children,omitempty"` Parents []Parent `json:"parents,omitempty"` @@ -79,15 +76,10 @@ func (c *ControlInstance) CodecDecodeSelf(decoder *codec.Decoder) { // MakeNode transforms a renderable node to a detailed node. It uses // aggregate metadata, plus the set of origin node IDs, to produce tables. -func MakeNode(topologyID string, r report.Report, ns render.RenderableNodes, n render.RenderableNode) Node { +func MakeNode(topologyID string, r report.Report, ns report.Nodes, n report.Node) Node { summary, _ := MakeNodeSummary(n) - summary.ID = n.ID - summary.Label = n.LabelMajor - return Node{ NodeSummary: summary, - Rank: n.Rank, - Pseudo: n.Pseudo, Controls: controls(r, n), Children: children(n), Parents: Parents(r, n), @@ -121,15 +113,10 @@ func controlsFor(topology report.Topology, nodeID string) []ControlInstance { return result } -func controls(r report.Report, n render.RenderableNode) []ControlInstance { - if _, ok := r.Process.Nodes[n.ControlNode]; ok { - return controlsFor(r.Process, n.ControlNode) - } else if _, ok := r.Container.Nodes[n.ControlNode]; ok { - return controlsFor(r.Container, n.ControlNode) - } else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok { - return controlsFor(r.ContainerImage, n.ControlNode) - } else if _, ok := r.Host.Nodes[n.ControlNode]; ok { - return controlsFor(r.Host, n.ControlNode) +func controls(r report.Report, n report.Node) []ControlInstance { + // TODO(paulbellamy): this ID will have been munged in rendering, so we should stop doing that, so that this matches up. + if t, ok := r.Topology(n.Topology); ok { + return controlsFor(t, n.ID) } return []ControlInstance{} } @@ -184,21 +171,24 @@ var ( TopologyID: "containers-by-image", Label: "Container Images", Columns: []Column{ - {ID: render.ContainersKey, Label: Label(render.ContainersKey), DefaultSort: true}, + {ID: report.Container, Label: Label(report.Container), DefaultSort: true}, }, }, }, } ) -func children(n render.RenderableNode) []NodeSummaryGroup { +func children(n report.Node) []NodeSummaryGroup { summaries := map[string][]NodeSummary{} - n.Children.ForEach(func(child render.RenderableNode) { - if child.ID != n.ID { - if summary, ok := MakeNodeSummary(child); ok { - summaries[child.Topology] = append(summaries[child.Topology], summary) - } + n.Children.ForEach(func(child report.Node) { + if child.ID == n.ID { + return + } + summary, ok := MakeNodeSummary(child) + if !ok { + return } + summaries[child.Topology] = append(summaries[child.Topology], summary.SummarizeMetrics()) }) nodeSummaryGroups := []NodeSummaryGroup{} diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index 2075b377cc..91d5adc0c0 100644 --- a/render/detailed/node_test.go +++ b/render/detailed/node_test.go @@ -1,8 +1,6 @@ package detailed_test import ( - "fmt" - "reflect" "testing" "github.com/weaveworks/scope/probe/docker" @@ -10,35 +8,41 @@ import ( "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/render/detailed" - "github.com/weaveworks/scope/render/expected" + "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/test" "github.com/weaveworks/scope/test/fixture" + "github.com/weaveworks/scope/test/reflect" ) +func child(t *testing.T, r render.Renderer, id string) detailed.NodeSummary { + s, ok := detailed.MakeNodeSummary(r.Render(fixture.Report)[id]) + if !ok { + t.Fatalf("Expected node %s to be summarizable, but wasn't", id) + } + return s.SummarizeMetrics() +} + func TestMakeDetailedHostNode(t *testing.T) { renderableNodes := render.HostRenderer.Render(fixture.Report) - renderableNode := renderableNodes[render.MakeHostID(fixture.ClientHostID)] + renderableNode := renderableNodes[fixture.ClientHostNodeID] have := detailed.MakeNode("hosts", fixture.Report, renderableNodes, renderableNode) - containerImageNodeSummary, _ := detailed.MakeNodeSummary( - render.ContainerImageRenderer.Render(fixture.Report)[expected.ClientContainerImageID], - ) - containerNodeSummary, _ := detailed.MakeNodeSummary( - render.ContainerRenderer.Render(fixture.Report)[expected.ClientContainerID], - ) - process1NodeSummary, _ := detailed.MakeNodeSummary( - render.ProcessRenderer.Render(fixture.Report)[expected.ClientProcess1ID], - ) + containerImageNodeSummary := child(t, render.ContainerImageRenderer, fixture.ClientContainerImageNodeID) + containerNodeSummary := child(t, render.ContainerRenderer, fixture.ClientContainerNodeID) + process1NodeSummary := child(t, render.ProcessRenderer, fixture.ClientProcess1NodeID) process1NodeSummary.Linkable = true - process2NodeSummary, _ := detailed.MakeNodeSummary( - render.ProcessRenderer.Render(fixture.Report)[expected.ClientProcess2ID], - ) + process2NodeSummary := child(t, render.ProcessRenderer, fixture.ClientProcess2NodeID) process2NodeSummary.Linkable = true want := detailed.Node{ NodeSummary: detailed.NodeSummary{ - ID: render.MakeHostID(fixture.ClientHostID), - Label: "client", - Linkable: true, + ID: fixture.ClientHostNodeID, + Label: "client", + LabelMinor: "hostname.com", + Rank: "hostname.com", + Pseudo: false, + Shape: "circle", + Linkable: true, + Adjacency: report.MakeIDList(fixture.ServerHostNodeID), Metadata: []detailed.MetadataRow{ { ID: "host_name", @@ -86,8 +90,6 @@ func TestMakeDetailedHostNode(t *testing.T) { }, }, }, - Rank: "hostname.com", - Pseudo: false, Controls: []detailed.ControlInstance{}, Children: []detailed.NodeSummaryGroup{ { @@ -106,7 +108,7 @@ func TestMakeDetailedHostNode(t *testing.T) { Label: "Container Images", TopologyID: "containers-by-image", Columns: []detailed.Column{ - {ID: render.ContainersKey, Label: detailed.Label(render.ContainersKey), DefaultSort: true}, + {ID: report.Container, Label: detailed.Label(report.Container), DefaultSort: true}, }, Nodes: []detailed.NodeSummary{containerImageNodeSummary}, }, @@ -126,9 +128,13 @@ func TestMakeDetailedHostNode(t *testing.T) { Columns: detailed.NormalColumns, Nodes: []detailed.NodeSummary{ { - ID: "host:server.hostname.com", - Label: "server", - Linkable: true, + ID: fixture.ServerHostNodeID, + Label: "server", + LabelMinor: "hostname.com", + Rank: "hostname.com", + Shape: "circle", + Linkable: true, + Adjacency: report.MakeIDList(render.OutgoingInternetID), Metadata: []detailed.MetadataRow{ { ID: "port", @@ -152,25 +158,31 @@ func TestMakeDetailedHostNode(t *testing.T) { } func TestMakeDetailedContainerNode(t *testing.T) { - id := render.MakeContainerID(fixture.ServerContainerID) + id := fixture.ServerContainerNodeID renderableNodes := render.ContainerRenderer.Render(fixture.Report) renderableNode, ok := renderableNodes[id] if !ok { t.Fatalf("Node not found: %s", id) } have := detailed.MakeNode("containers", fixture.Report, renderableNodes, renderableNode) + + serverProcessNodeSummary := child(t, render.ProcessRenderer, fixture.ServerProcessNodeID) + serverProcessNodeSummary.Linkable = true want := detailed.Node{ NodeSummary: detailed.NodeSummary{ - ID: id, - Label: "server", - Linkable: true, + ID: id, + Label: "server", + LabelMinor: "server.hostname.com", + Shape: "hexagon", + Linkable: true, + Pseudo: false, Metadata: []detailed.MetadataRow{ {ID: "docker_container_id", Value: fixture.ServerContainerID, Prime: true}, {ID: "docker_container_state", Value: "running", Prime: true}, {ID: "docker_image_id", Value: fixture.ServerContainerImageID}, }, DockerLabels: []detailed.MetadataRow{ - {ID: "label_" + render.AmazonECSContainerNameLabel, Value: `server`}, + {ID: "label_" + detailed.AmazonECSContainerNameLabel, Value: `server`}, {ID: "label_foo1", Value: `bar1`}, {ID: "label_foo2", Value: `bar2`}, {ID: "label_io.kubernetes.pod.name", Value: "ping/pong-b"}, @@ -190,34 +202,23 @@ func TestMakeDetailedContainerNode(t *testing.T) { }, }, }, - Pseudo: false, Controls: []detailed.ControlInstance{}, Children: []detailed.NodeSummaryGroup{ { Label: "Processes", TopologyID: "processes", Columns: []detailed.Column{detailed.MakeColumn(process.PID), detailed.MakeColumn(process.CPUUsage), detailed.MakeColumn(process.MemoryUsage)}, - Nodes: []detailed.NodeSummary{ - { - ID: fmt.Sprintf("process:%s:%s", "server.hostname.com", fixture.ServerPID), - Label: "apache", - Linkable: true, - Metadata: []detailed.MetadataRow{ - {ID: process.PID, Value: fixture.ServerPID, Prime: true, Datatype: "number"}, - }, - Metrics: []detailed.MetricRow{}, - }, - }, + Nodes: []detailed.NodeSummary{serverProcessNodeSummary}, }, }, Parents: []detailed.Parent{ { - ID: render.MakeContainerImageID(fixture.ServerContainerImageName), + ID: fixture.ServerContainerImageNodeID, Label: fixture.ServerContainerImageName, TopologyID: "containers-by-image", }, { - ID: render.MakeHostID(fixture.ServerHostName), + ID: fixture.ServerHostNodeID, Label: fixture.ServerHostName, TopologyID: "hosts", }, @@ -230,9 +231,12 @@ func TestMakeDetailedContainerNode(t *testing.T) { Columns: detailed.NormalColumns, Nodes: []detailed.NodeSummary{ { - ID: "container:a1b2c3d4e5", - Label: "client", - Linkable: true, + ID: fixture.ClientContainerNodeID, + Label: "client", + LabelMinor: "client.hostname.com", + Shape: "hexagon", + Linkable: true, + Adjacency: report.MakeIDList(fixture.ServerContainerNodeID), Metadata: []detailed.MetadataRow{ { ID: "port", @@ -247,9 +251,14 @@ func TestMakeDetailedContainerNode(t *testing.T) { }, }, { - ID: "in-theinternet", - Label: "The Internet", - Linkable: true, + ID: render.IncomingInternetID, + Label: render.InboundMajor, + LabelMinor: render.InboundMinor, + Rank: render.IncomingInternetID, + Shape: "cloud", + Linkable: true, + Pseudo: true, + Adjacency: report.MakeIDList(fixture.ServerContainerNodeID), Metadata: []detailed.MetadataRow{ { ID: "port", diff --git a/render/detailed/parents.go b/render/detailed/parents.go index ecdb2bdcf0..991820e097 100644 --- a/render/detailed/parents.go +++ b/render/detailed/parents.go @@ -6,7 +6,6 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/kubernetes" - "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) @@ -19,7 +18,7 @@ type Parent struct { // Parents renders the parents of this report.Node, which have been aggregated // from the probe reports. -func Parents(r report.Report, n render.RenderableNode) (result []Parent) { +func Parents(r report.Report, n report.Node) (result []Parent) { topologies := map[string]struct { report.Topology render func(report.Node) Parent @@ -37,9 +36,9 @@ func Parents(r report.Report, n render.RenderableNode) (result []Parent) { sort.Strings(topologyIDs) for _, topologyID := range topologyIDs { t := topologies[topologyID] - parents, _ := n.Node.Parents.Lookup(topologyID) + parents, _ := n.Parents.Lookup(topologyID) for _, id := range parents { - if topologyID == n.Node.Topology && id == n.Node.ID { + if topologyID == n.Topology && id == n.ID { continue } @@ -55,39 +54,42 @@ func Parents(r report.Report, n render.RenderableNode) (result []Parent) { } func containerParent(n report.Node) Parent { - label, _ := render.GetRenderableContainerName(n) + label := getRenderableContainerName(n) containerID, _ := n.Latest.Lookup(docker.ContainerID) return Parent{ - ID: render.MakeContainerID(containerID), + ID: report.MakeContainerNodeID(containerID), Label: label, TopologyID: "containers", } } func podParent(n report.Node) Parent { + namespace, _ := n.Latest.Lookup(kubernetes.Namespace) podID, _ := n.Latest.Lookup(kubernetes.PodID) podName, _ := n.Latest.Lookup(kubernetes.PodName) return Parent{ - ID: render.MakePodID(podID), + ID: report.MakePodNodeID(namespace, podID), Label: podName, TopologyID: "pods", } } func serviceParent(n report.Node) Parent { + namespace, _ := n.Latest.Lookup(kubernetes.Namespace) serviceID, _ := n.Latest.Lookup(kubernetes.ServiceID) serviceName, _ := n.Latest.Lookup(kubernetes.ServiceName) return Parent{ - ID: render.MakeServiceID(serviceID), + ID: report.MakeServiceNodeID(namespace, serviceID), Label: serviceName, TopologyID: "pods-by-service", } } func containerImageParent(n report.Node) Parent { + imageID, _ := n.Latest.Lookup(docker.ImageID) imageName, _ := n.Latest.Lookup(docker.ImageName) return Parent{ - ID: render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)), + ID: report.MakeContainerImageNodeID(imageID), Label: imageName, TopologyID: "containers-by-image", } @@ -96,7 +98,7 @@ func containerImageParent(n report.Node) Parent { func hostParent(n report.Node) Parent { hostName, _ := n.Latest.Lookup(host.HostName) return Parent{ - ID: render.MakeHostID(hostName), + ID: report.MakeHostNodeID(hostName), Label: hostName, TopologyID: "hosts", } diff --git a/render/detailed/parents_test.go b/render/detailed/parents_test.go index 572e750560..7a21b18bac 100644 --- a/render/detailed/parents_test.go +++ b/render/detailed/parents_test.go @@ -15,33 +15,39 @@ import ( func TestParents(t *testing.T) { for _, c := range []struct { name string - node render.RenderableNode + node report.Node want []detailed.Parent }{ { name: "Node accidentally tagged with itself", - node: render.HostRenderer.Render(fixture.Report)[render.MakeHostID(fixture.ClientHostID)].WithParents( + node: render.HostRenderer.Render(fixture.Report)[fixture.ClientHostNodeID].WithParents( report.EmptySets.Add(report.Host, report.MakeStringSet(fixture.ClientHostNodeID)), ), want: nil, }, { - node: render.HostRenderer.Render(fixture.Report)[render.MakeHostID(fixture.ClientHostID)], + node: render.HostRenderer.Render(fixture.Report)[fixture.ClientHostNodeID], want: nil, }, { - node: render.ContainerRenderer.Render(fixture.Report)[render.MakeContainerID(fixture.ClientContainerID)], + node: render.ContainerImageRenderer.Render(fixture.Report)[fixture.ClientContainerImageNodeID], want: []detailed.Parent{ - {ID: render.MakeContainerImageID(fixture.ClientContainerImageName), Label: fixture.ClientContainerImageName, TopologyID: "containers-by-image"}, - {ID: render.MakeHostID(fixture.ClientHostID), Label: fixture.ClientHostName, TopologyID: "hosts"}, + {ID: fixture.ClientHostNodeID, Label: fixture.ClientHostName, TopologyID: "hosts"}, }, }, { - node: render.ProcessRenderer.Render(fixture.Report)[render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID)], + node: render.ContainerRenderer.Render(fixture.Report)[fixture.ClientContainerNodeID], want: []detailed.Parent{ - {ID: render.MakeContainerID(fixture.ClientContainerID), Label: fixture.ClientContainerName, TopologyID: "containers"}, - {ID: render.MakeContainerImageID(fixture.ClientContainerImageName), Label: fixture.ClientContainerImageName, TopologyID: "containers-by-image"}, - {ID: render.MakeHostID(fixture.ClientHostID), Label: fixture.ClientHostName, TopologyID: "hosts"}, + {ID: fixture.ClientContainerImageNodeID, Label: fixture.ClientContainerImageName, TopologyID: "containers-by-image"}, + {ID: fixture.ClientHostNodeID, Label: fixture.ClientHostName, TopologyID: "hosts"}, + }, + }, + { + node: render.ProcessRenderer.Render(fixture.Report)[fixture.ClientProcess1NodeID], + want: []detailed.Parent{ + {ID: fixture.ClientContainerNodeID, Label: fixture.ClientContainerName, TopologyID: "containers"}, + {ID: fixture.ClientContainerImageNodeID, Label: fixture.ClientContainerImageName, TopologyID: "containers-by-image"}, + {ID: fixture.ClientHostNodeID, Label: fixture.ClientHostName, TopologyID: "hosts"}, }, }, } { diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 473db69c68..a61506dbb6 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -2,8 +2,10 @@ package detailed import ( "fmt" + "strings" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/endpoint" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" @@ -11,6 +13,21 @@ import ( "github.com/weaveworks/scope/report" ) +// Shapes that are allowed +const ( + Circle = "circle" + Square = "square" + Heptagon = "heptagon" + Hexagon = "hexagon" + Cloud = "cloud" + + ImageNameNone = "" + + // Keys we use to render container names + AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name" + KubernetesContainerNameLabel = "io.kubernetes.container.name" +) + // NodeSummaryGroup is a topology-typed group of children for a Node. type NodeSummaryGroup struct { ID string `json:"id"` @@ -53,33 +70,56 @@ func MakeColumn(id string) Column { type NodeSummary struct { ID string `json:"id"` Label string `json:"label"` - Linkable bool `json:"linkable"` // Whether this node can be linked-to + LabelMinor string `json:"label_minor"` + Rank string `json:"rank"` + Shape string `json:"shape,omitempty"` + Stack bool `json:"stack,omitempty"` + Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to + Pseudo bool `json:"pseudo,omitempty"` Metadata []MetadataRow `json:"metadata,omitempty"` DockerLabels []MetadataRow `json:"docker_labels,omitempty"` Metrics []MetricRow `json:"metrics,omitempty"` + Adjacency report.IDList `json:"adjacency,omitempty"` } // MakeNodeSummary summarizes a node, if possible. -func MakeNodeSummary(n render.RenderableNode) (NodeSummary, bool) { - renderers := map[string]func(report.Node) NodeSummary{ +func MakeNodeSummary(n report.Node) (NodeSummary, bool) { + renderers := map[string]func(NodeSummary, report.Node) (NodeSummary, bool){ + render.Pseudo: pseudoNodeSummary, report.Process: processNodeSummary, report.Container: containerNodeSummary, report.ContainerImage: containerImageNodeSummary, report.Pod: podNodeSummary, + report.Service: serviceNodeSummary, report.Host: hostNodeSummary, } if renderer, ok := renderers[n.Topology]; ok { - return renderer(n.Node), true + return renderer(baseNodeSummary(n), n) } return NodeSummary{}, false } +// SummarizeMetrics returns a copy of the NodeSummary where the metrics are +// replaced with their summaries +func (n NodeSummary) SummarizeMetrics() NodeSummary { + cp := n.Copy() + for i, m := range cp.Metrics { + cp.Metrics[i] = m.Summary() + } + return cp +} + // Copy returns a value copy of the NodeSummary func (n NodeSummary) Copy() NodeSummary { result := NodeSummary{ - ID: n.ID, - Label: n.Label, - Linkable: n.Linkable, + ID: n.ID, + Label: n.Label, + LabelMinor: n.LabelMinor, + Rank: n.Rank, + Shape: n.Shape, + Stack: n.Stack, + Linkable: n.Linkable, + Adjacency: n.Adjacency.Copy(), } for _, row := range n.Metadata { result.Metadata = append(result.Metadata, row.Copy()) @@ -93,52 +133,202 @@ func (n NodeSummary) Copy() NodeSummary { return result } -func baseNodeSummary(id, label string, linkable bool, nmd report.Node) NodeSummary { +func baseNodeSummary(n report.Node) NodeSummary { return NodeSummary{ - ID: id, - Label: label, - Linkable: linkable, - Metadata: NodeMetadata(nmd), - DockerLabels: NodeDockerLabels(nmd), - Metrics: NodeMetrics(nmd), + ID: n.ID, + Shape: Circle, + Linkable: true, + Metadata: NodeMetadata(n), + DockerLabels: NodeDockerLabels(n), + Metrics: NodeMetrics(n), + Adjacency: n.Adjacency.Copy(), } } -func processNodeSummary(nmd report.Node) NodeSummary { - var ( - id string - label, nameFound = nmd.Latest.Lookup(process.Name) - ) - if pid, ok := nmd.Latest.Lookup(process.PID); ok { - if !nameFound { - label = fmt.Sprintf("(%s)", pid) +func pseudoNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Pseudo = true + base.Rank = n.ID + + if template, ok := map[string]struct{ Label, LabelMinor, Shape string }{ + render.TheInternetID: {render.InboundMajor, "", Cloud}, + render.IncomingInternetID: {render.InboundMajor, render.InboundMinor, Cloud}, + render.OutgoingInternetID: {render.OutboundMajor, render.OutboundMinor, Cloud}, + }[n.ID]; ok { + base.Label = template.Label + base.LabelMinor = template.LabelMinor + base.Shape = template.Shape + return base, true + } + + // try rendering it as an uncontained node + if strings.HasPrefix(n.ID, render.MakePseudoNodeID(render.UncontainedID)) { + base.Label = render.UncontainedMajor + base.Shape = Square + base.Stack = true + base.LabelMinor = report.ExtractHostID(n) + return base, true + } + + // try rendering it as an endpoint + if addr, ok := n.Latest.Lookup(endpoint.Addr); ok { + base.Label = addr + return base, true + } + + return NodeSummary{}, false +} + +func processNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(process.Name) + base.Rank, _ = n.Latest.Lookup(process.Name) + base.Shape = Square + + if p, ok := n.Counters.Lookup(report.Process); ok { + base.Stack = true + if p == 1 { + base.LabelMinor = fmt.Sprintf("%d process", p) + } else { + base.LabelMinor = fmt.Sprintf("%d processes", p) + } + } else { + pid, ok := n.Latest.Lookup(process.PID) + if !ok { + return NodeSummary{}, false + } + if containerName, ok := n.Latest.Lookup(docker.ContainerName); ok { + base.LabelMinor = fmt.Sprintf("%s (%s:%s)", report.ExtractHostID(n), containerName, pid) + } else { + base.LabelMinor = fmt.Sprintf("%s (%s)", report.ExtractHostID(n), pid) } - id = render.MakeProcessID(report.ExtractHostID(nmd), pid) } - _, isConnected := nmd.Latest.Lookup(render.IsConnected) - return baseNodeSummary(id, label, isConnected, nmd) + + _, isConnected := n.Latest.Lookup(render.IsConnected) + base.Linkable = isConnected + return base, true } -func containerNodeSummary(nmd report.Node) NodeSummary { - label, _ := render.GetRenderableContainerName(nmd) - containerID, _ := nmd.Latest.Lookup(docker.ContainerID) - return baseNodeSummary(render.MakeContainerID(containerID), label, true, nmd) +func containerNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label = getRenderableContainerName(n) + + if c, ok := n.Counters.Lookup(report.Container); ok { + base.Stack = true + if c == 1 { + base.LabelMinor = fmt.Sprintf("%d container", c) + } else { + base.LabelMinor = fmt.Sprintf("%d containers", c) + } + } else { + base.LabelMinor = report.ExtractHostID(n) + } + + if imageName, ok := n.Latest.Lookup(docker.ImageName); ok { + base.Rank = render.ImageNameWithoutVersion(imageName) + } + + base.Shape = Hexagon + return base, true } -func containerImageNodeSummary(nmd report.Node) NodeSummary { - imageName, _ := nmd.Latest.Lookup(docker.ImageName) - return baseNodeSummary(render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)), imageName, true, nmd) +func containerImageNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + imageName, ok := n.Latest.Lookup(docker.ImageName) + if !ok { + return NodeSummary{}, false + } + + imageNameWithoutVersion := render.ImageNameWithoutVersion(imageName) + base.Label = imageNameWithoutVersion + base.Rank = imageNameWithoutVersion + base.Shape = Hexagon + base.Stack = true + + if base.Label == ImageNameNone { + base.Label, _ = n.Latest.Lookup(docker.ImageID) + if len(base.Label) > 12 { + base.Label = base.Label[:12] + } + } + + if i, ok := n.Counters.Lookup(report.ContainerImage); ok { + if i == 1 { + base.LabelMinor = fmt.Sprintf("%d image", i) + } else { + base.LabelMinor = fmt.Sprintf("%d images", i) + } + } else if c, ok := n.Counters.Lookup(report.Container); ok { + if c == 1 { + base.LabelMinor = fmt.Sprintf("%d container", c) + } else { + base.LabelMinor = fmt.Sprintf("%d containers", c) + } + } + return base, true } -func podNodeSummary(nmd report.Node) NodeSummary { - podID, _ := nmd.Latest.Lookup(kubernetes.PodID) - podName, _ := nmd.Latest.Lookup(kubernetes.PodName) - return baseNodeSummary(render.MakePodID(podID), podName, true, nmd) +func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(kubernetes.PodName) + base.Rank, _ = n.Latest.Lookup(kubernetes.PodID) + base.Shape = Heptagon + + if p, ok := n.Counters.Lookup(report.Pod); ok { + base.Stack = true + if p == 1 { + base.LabelMinor = fmt.Sprintf("%d pod", p) + } else { + base.LabelMinor = fmt.Sprintf("%d pods", p) + } + } else if c, ok := n.Counters.Lookup(report.Container); ok { + if c == 1 { + base.LabelMinor = fmt.Sprintf("%d container", c) + } else { + base.LabelMinor = fmt.Sprintf("%d containers", c) + } + } + + return base, true } -func hostNodeSummary(nmd report.Node) NodeSummary { - hostName, _ := nmd.Latest.Lookup(host.HostName) - return baseNodeSummary(render.MakeHostID(hostName), hostName, true, nmd) +func serviceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(kubernetes.ServiceName) + base.Rank, _ = n.Latest.Lookup(kubernetes.ServiceID) + base.Shape = Heptagon + base.Stack = true + + // Services are always just a group of pods, so there's no counting multiple + // services which might be grouped together. + if p, ok := n.Counters.Lookup(report.Pod); ok { + if p == 1 { + base.LabelMinor = fmt.Sprintf("%d pod", p) + } else { + base.LabelMinor = fmt.Sprintf("%d pods", p) + } + } + + return base, true +} + +func hostNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + var ( + hostname, _ = n.Latest.Lookup(host.HostName) + parts = strings.SplitN(hostname, ".", 2) + ) + + if len(parts) == 2 { + base.Label, base.LabelMinor, base.Rank = parts[0], parts[1], parts[1] + } else { + base.Label = hostname + } + + if h, ok := n.Counters.Lookup(report.Host); ok { + base.Stack = true + if h == 1 { + base.LabelMinor = fmt.Sprintf("%d host", h) + } else { + base.LabelMinor = fmt.Sprintf("%d hosts", h) + } + } + + base.Shape = Circle + return base, true } type nodeSummariesByID []NodeSummary @@ -146,3 +336,54 @@ type nodeSummariesByID []NodeSummary func (s nodeSummariesByID) Len() int { return len(s) } func (s nodeSummariesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID } + +// NodeSummaries is a set of NodeSummaries indexed by ID. +type NodeSummaries map[string]NodeSummary + +// Summaries converts RenderableNodes into a set of NodeSummaries +func Summaries(rns report.Nodes) NodeSummaries { + result := NodeSummaries{} + for id, node := range rns { + if summary, ok := MakeNodeSummary(node); ok { + for i, m := range summary.Metrics { + summary.Metrics[i] = m.Summary() + } + result[id] = summary + } + } + return result +} + +// Copy returns a deep value-copy of NodeSummaries +func (n NodeSummaries) Copy() NodeSummaries { + result := NodeSummaries{} + for k, v := range n { + result[k] = v.Copy() + } + return result +} + +// getRenderableContainerName obtains a user-friendly container name, to render in the UI +func getRenderableContainerName(nmd report.Node) string { + for _, key := range []string{ + // Amazon's ecs-agent produces huge Docker container names, destructively + // derived from mangling Container Definition names in Task + // Definitions. + // + // However, the ecs-agent provides a label containing the original Container + // Definition name. + docker.LabelPrefix + AmazonECSContainerNameLabel, + // Kubernetes also mangles its Docker container names and provides a + // label with the original container name. However, note that this label + // is only provided by Kubernetes versions >= 1.2 (see + // https://github.com/kubernetes/kubernetes/pull/17234/ ) + docker.LabelPrefix + KubernetesContainerNameLabel, + docker.ContainerName, + docker.ContainerHostname, + } { + if label, ok := nmd.Latest.Lookup(key); ok { + return label + } + } + return "" +} diff --git a/render/detailed/summary_test.go b/render/detailed/summary_test.go new file mode 100644 index 0000000000..db1c362a41 --- /dev/null +++ b/render/detailed/summary_test.go @@ -0,0 +1,186 @@ +package detailed_test + +import ( + "sort" + "testing" + "time" + + "github.com/weaveworks/scope/common/mtime" + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/probe/process" + "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/render/detailed" + "github.com/weaveworks/scope/render/expected" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" + "github.com/weaveworks/scope/test/fixture" + "github.com/weaveworks/scope/test/reflect" +) + +func TestSummaries(t *testing.T) { + { + // Just a convenient source of some rendered nodes + have := detailed.Summaries(render.ProcessRenderer.Render(fixture.Report)) + // The ids of the processes rendered above + expectedIDs := []string{ + fixture.ClientProcess1NodeID, + fixture.ClientProcess2NodeID, + fixture.ServerProcessNodeID, + fixture.NonContainerProcessNodeID, + expected.UnknownPseudoNode1ID, + expected.UnknownPseudoNode2ID, + render.IncomingInternetID, + render.OutgoingInternetID, + } + sort.Strings(expectedIDs) + + // It should summarize each node + ids := []string{} + for id := range have { + ids = append(ids, id) + } + sort.Strings(ids) + if !reflect.DeepEqual(expectedIDs, ids) { + t.Fatalf("Expected Summaries to have summarized every node in the process renderer: %v, but got %v", expectedIDs, ids) + } + } + + // It should summarize nodes' metrics + { + t1, t2 := mtime.Now().Add(-1*time.Minute), mtime.Now() + metric := report.MakeMetric().Add(t1, 1).Add(t2, 2) + input := fixture.Report.Copy() + + input.Process.Nodes[fixture.ClientProcess1NodeID] = input.Process.Nodes[fixture.ClientProcess1NodeID].WithMetrics(report.Metrics{process.CPUUsage: metric}) + have := detailed.Summaries(render.ProcessRenderer.Render(input)) + + node, ok := have[fixture.ClientProcess1NodeID] + if !ok { + t.Fatalf("Expected output to have the node we added the metric to") + } + + var row detailed.MetricRow + ok = false + for _, metric := range node.Metrics { + if metric.ID == process.CPUUsage { + row = metric + ok = true + break + } + } + if !ok { + t.Fatalf("Expected node to have the metric we added") + } + + // Our summarized MetricRow + want := detailed.MetricRow{ + ID: process.CPUUsage, + Format: "percent", + Value: 2, + Metric: &report.Metric{ + Samples: nil, + Min: metric.Min, + Max: metric.Max, + First: metric.First, + Last: metric.Last, + }, + } + if !reflect.DeepEqual(want, row) { + t.Fatalf("Expected to have summarized the node's metrics: %s", test.Diff(want, row)) + } + } +} + +func TestMakeNodeSummary(t *testing.T) { + testcases := []struct { + name string + input report.Node + ok bool + want detailed.NodeSummary + }{ + { + name: "single process rendering", + input: expected.RenderedProcesses[fixture.ClientProcess1NodeID], + ok: true, + want: detailed.NodeSummary{ + ID: fixture.ClientProcess1NodeID, + Label: fixture.Client1Name, + LabelMinor: "client.hostname.com (10001)", + Rank: fixture.Client1Name, + Shape: "square", + Metadata: []detailed.MetadataRow{ + {ID: process.PID, Value: fixture.Client1PID, Prime: true, Datatype: "number"}, + }, + Metrics: []detailed.MetricRow{}, + Adjacency: report.MakeIDList(fixture.ServerProcessNodeID), + }, + }, + { + name: "single container rendering", + input: expected.RenderedContainers[fixture.ClientContainerNodeID], + ok: true, + want: detailed.NodeSummary{ + ID: fixture.ClientContainerNodeID, + Label: fixture.ClientContainerName, + LabelMinor: fixture.ClientHostName, + Rank: fixture.ClientContainerImageName, + Shape: "hexagon", + Linkable: true, + Metadata: []detailed.MetadataRow{ + {ID: docker.ContainerID, Value: fixture.ClientContainerID, Prime: true}, + }, + Metrics: []detailed.MetricRow{}, + Adjacency: report.MakeIDList(fixture.ServerContainerNodeID), + }, + }, + { + name: "single container image rendering", + input: expected.RenderedContainerImages[fixture.ClientContainerImageNodeID], + ok: true, + want: detailed.NodeSummary{ + ID: fixture.ClientContainerImageNodeID, + Label: fixture.ClientContainerImageName, + LabelMinor: "1 container", + Rank: fixture.ClientContainerImageName, + Shape: "hexagon", + Linkable: true, + Stack: true, + Metadata: []detailed.MetadataRow{ + {ID: docker.ImageID, Value: fixture.ClientContainerImageID, Prime: true}, + {ID: report.Container, Value: "1", Prime: true, Datatype: "number"}, + }, + Adjacency: report.MakeIDList(fixture.ServerContainerImageNodeID), + }, + }, + { + name: "single host rendering", + input: expected.RenderedHosts[fixture.ClientHostNodeID], + ok: true, + want: detailed.NodeSummary{ + ID: fixture.ClientHostNodeID, + Label: "client", + LabelMinor: "hostname.com", + Rank: "hostname.com", + Shape: "circle", + Linkable: true, + Metadata: []detailed.MetadataRow{ + {ID: host.HostName, Value: fixture.ClientHostName, Prime: false}, + }, + Metrics: []detailed.MetricRow{}, + Adjacency: report.MakeIDList(fixture.ServerHostNodeID), + }, + }, + } + for _, testcase := range testcases { + have, ok := detailed.MakeNodeSummary(testcase.input) + if ok != testcase.ok { + t.Errorf("%s: MakeNodeSummary failed: expected ok value to be: %v", testcase.name, testcase.ok) + continue + } + + if !reflect.DeepEqual(testcase.want, have) { + t.Errorf("%s: Node Summary did not match: %s", testcase.name, test.Diff(testcase.want, have)) + } + } +} diff --git a/render/topology_diff.go b/render/detailed/topology_diff.go similarity index 74% rename from render/topology_diff.go rename to render/detailed/topology_diff.go index 9e0c552534..66836d5b89 100644 --- a/render/topology_diff.go +++ b/render/detailed/topology_diff.go @@ -1,19 +1,19 @@ -package render +package detailed import ( "reflect" ) // Diff is returned by TopoDiff. It represents the changes between two -// RenderableNode maps. +// NodeSummary maps. type Diff struct { - Add []RenderableNode `json:"add"` - Update []RenderableNode `json:"update"` - Remove []string `json:"remove"` + Add []NodeSummary `json:"add"` + Update []NodeSummary `json:"update"` + Remove []string `json:"remove"` } // TopoDiff gives you the diff to get from A to B. -func TopoDiff(a, b RenderableNodes) Diff { +func TopoDiff(a, b NodeSummaries) Diff { diff := Diff{} notSeen := map[string]struct{}{} diff --git a/render/detailed/topology_diff_test.go b/render/detailed/topology_diff_test.go new file mode 100644 index 0000000000..dfdc360cda --- /dev/null +++ b/render/detailed/topology_diff_test.go @@ -0,0 +1,90 @@ +package detailed_test + +import ( + "reflect" + "sort" + "testing" + + "github.com/weaveworks/scope/render/detailed" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" +) + +// ByID is a sort interface for a NodeSummary slice. +type ByID []detailed.NodeSummary + +func (r ByID) Len() int { return len(r) } +func (r ByID) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r ByID) Less(i, j int) bool { return r[i].ID < r[j].ID } + +func TestTopoDiff(t *testing.T) { + nodea := detailed.NodeSummary{ + ID: "nodea", + Label: "Node A", + LabelMinor: "'ts an a", + Pseudo: false, + Adjacency: report.MakeIDList("nodeb"), + } + nodeap := nodea.Copy() + nodeap.Adjacency = report.MakeIDList("nodeb", "nodeq") // not the same anymore + nodeb := detailed.NodeSummary{ + ID: "nodeb", + Label: "Node B", + } + + // Helper to make RenderableNode maps. + nodes := func(ns ...detailed.NodeSummary) detailed.NodeSummaries { + r := detailed.NodeSummaries{} + for _, n := range ns { + r[n.ID] = n + } + return r + } + + for _, c := range []struct { + label string + have, want detailed.Diff + }{ + { + label: "basecase: empty -> something", + have: detailed.TopoDiff(nodes(), nodes(nodea, nodeb)), + want: detailed.Diff{ + Add: []detailed.NodeSummary{nodea, nodeb}, + }, + }, + { + label: "basecase: something -> empty", + have: detailed.TopoDiff(nodes(nodea, nodeb), nodes()), + want: detailed.Diff{ + Remove: []string{"nodea", "nodeb"}, + }, + }, + { + label: "add and remove", + have: detailed.TopoDiff(nodes(nodea), nodes(nodeb)), + want: detailed.Diff{ + Add: []detailed.NodeSummary{nodeb}, + Remove: []string{"nodea"}, + }, + }, + { + label: "no change", + have: detailed.TopoDiff(nodes(nodea), nodes(nodea)), + want: detailed.Diff{}, + }, + { + label: "change a single node", + have: detailed.TopoDiff(nodes(nodea), nodes(nodeap)), + want: detailed.Diff{ + Update: []detailed.NodeSummary{nodeap}, + }, + }, + } { + sort.Strings(c.have.Remove) + sort.Sort(ByID(c.have.Add)) + sort.Sort(ByID(c.have.Update)) + if !reflect.DeepEqual(c.want, c.have) { + t.Errorf("%s - %s", c.label, test.Diff(c.want, c.have)) + } + } +} diff --git a/render/expected/expected.go b/render/expected/expected.go index 20f096eecf..fe7d60c0ea 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -1,8 +1,9 @@ package expected import ( - "fmt" - + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/test/fixture" @@ -16,566 +17,260 @@ var ( hexagon = "hexagon" cloud = "cloud" - Client54001EndpointID = render.MakeEndpointID(fixture.ClientHostID, fixture.ClientIP, fixture.ClientPort54001) - Client54002EndpointID = render.MakeEndpointID(fixture.ClientHostID, fixture.ClientIP, fixture.ClientPort54002) - ServerEndpointID = render.MakeEndpointID(fixture.ServerHostID, fixture.ServerIP, fixture.ServerPort) - UnknownClient1EndpointID = render.MakeEndpointID("", fixture.UnknownClient1IP, fixture.UnknownClient1Port) - UnknownClient2EndpointID = render.MakeEndpointID("", fixture.UnknownClient2IP, fixture.UnknownClient2Port) - UnknownClient3EndpointID = render.MakeEndpointID("", fixture.UnknownClient3IP, fixture.UnknownClient3Port) - RandomClientEndpointID = render.MakeEndpointID("", fixture.RandomClientIP, fixture.RandomClientPort) - NonContainerEndpointID = render.MakeEndpointID(fixture.ServerHostID, fixture.ServerIP, fixture.NonContainerClientPort) - GoogleEndpointID = render.MakeEndpointID("", fixture.GoogleIP, fixture.GooglePort) - - RenderedEndpoints = (render.RenderableNodes{ - Client54001EndpointID: { - ID: Client54001EndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(10), - EgressByteCount: newu64(100), - }, - }, - Client54002EndpointID: { - ID: Client54002EndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(20), - EgressByteCount: newu64(200), - }, - }, - ServerEndpointID: { - ID: ServerEndpointID, - Shape: circle, - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - UnknownClient1EndpointID: { - ID: UnknownClient1EndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - }, - UnknownClient2EndpointID: { - ID: UnknownClient2EndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(40), - EgressByteCount: newu64(400), - }, - }, - UnknownClient3EndpointID: { - ID: UnknownClient3EndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(50), - EgressByteCount: newu64(500), - }, - }, - RandomClientEndpointID: { - ID: RandomClientEndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(ServerEndpointID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(60), - EgressByteCount: newu64(600), - }, - }, - NonContainerEndpointID: { - ID: NonContainerEndpointID, - Shape: circle, - Node: report.MakeNode().WithAdjacent(GoogleEndpointID), - EdgeMetadata: report.EdgeMetadata{}, - }, - GoogleEndpointID: { - ID: GoogleEndpointID, - Shape: circle, - Node: report.MakeNode(), - }, - }).Prune() - - ClientProcess1ID = render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID) - ClientProcess2ID = render.MakeProcessID(fixture.ClientHostID, fixture.Client2PID) - ServerProcessID = render.MakeProcessID(fixture.ServerHostID, fixture.ServerPID) - nonContainerProcessID = render.MakeProcessID(fixture.ServerHostID, fixture.NonContainerPID) - unknownPseudoNode1ID = render.MakePseudoNodeID(fixture.UnknownClient1IP) - unknownPseudoNode2ID = render.MakePseudoNodeID(fixture.UnknownClient3IP) - - unknownPseudoNode1 = func(adjacent string) render.RenderableNode { - return render.RenderableNode{ - ID: unknownPseudoNode1ID, - LabelMajor: fixture.UnknownClient1IP, - Pseudo: true, - Shape: circle, - Node: report.MakeNode().WithAdjacent(adjacent), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[UnknownClient1EndpointID], - RenderedEndpoints[UnknownClient2EndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(70), - EgressByteCount: newu64(700), - }, + // Helper to make a report.node with some common options + node = func(topology string) func(id string, adjacent ...string) report.Node { + return func(id string, adjacent ...string) report.Node { + n := report.MakeNode().WithID(id).WithTopology(topology) + for _, a := range adjacent { + n = n.WithAdjacent(a) + } + return n } } - unknownPseudoNode2 = func(adjacent string) render.RenderableNode { - return render.RenderableNode{ - ID: unknownPseudoNode2ID, - LabelMajor: fixture.UnknownClient3IP, - Pseudo: true, - Shape: circle, - Node: report.MakeNode().WithAdjacent(adjacent), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[UnknownClient3EndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(50), - EgressByteCount: newu64(500), - }, - } + pseudo = node(render.Pseudo) + endpoint = node(report.Endpoint) + processNode = node(report.Process) + container = node(report.Container) + containerImage = node(report.ContainerImage) + pod = node(report.Pod) + service = node(report.Service) + hostNode = node(report.Host) + + UnknownPseudoNode1ID = render.MakePseudoNodeID(fixture.UnknownClient1IP) + UnknownPseudoNode2ID = render.MakePseudoNodeID(fixture.UnknownClient3IP) + + unknownPseudoNode1 = func(adjacent ...string) report.Node { + return pseudo(UnknownPseudoNode1ID, adjacent...). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.UnknownClient1NodeID], + RenderedEndpoints[fixture.UnknownClient2NodeID], + )) } - theIncomingInternetNode = func(adjacent string) render.RenderableNode { - return render.RenderableNode{ - ID: render.IncomingInternetID, - LabelMajor: render.InboundMajor, - LabelMinor: render.InboundMinor, - Pseudo: true, - Shape: cloud, - Node: report.MakeNode().WithAdjacent(adjacent), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[RandomClientEndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(60), - EgressByteCount: newu64(600), - }, - } + unknownPseudoNode2 = func(adjacent ...string) report.Node { + return pseudo(UnknownPseudoNode2ID, adjacent...). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.UnknownClient3NodeID], + )) } - theOutgoingInternetNode = render.RenderableNode{ - ID: render.OutgoingInternetID, - LabelMajor: render.OutboundMajor, - LabelMinor: render.OutboundMinor, - Pseudo: true, - Shape: cloud, - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{}, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[GoogleEndpointID], - ), + + theIncomingInternetNode = func(adjacent ...string) report.Node { + return pseudo(render.IncomingInternetID, adjacent...). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.RandomClientNodeID], + )) } - RenderedProcesses = (render.RenderableNodes{ - ClientProcess1ID: { - ID: ClientProcess1ID, - LabelMajor: fixture.Client1Name, - LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client1PID), - Rank: fixture.Client1Name, - Shape: square, - Node: report.MakeNode().WithAdjacent(ServerProcessID), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(10), - EgressByteCount: newu64(100), - }, - }, - ClientProcess2ID: { - ID: ClientProcess2ID, - LabelMajor: fixture.Client2Name, - LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client2PID), - Rank: fixture.Client2Name, - Shape: square, - Node: report.MakeNode().WithAdjacent(ServerProcessID), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54002EndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(20), - EgressByteCount: newu64(200), - }, - }, - ServerProcessID: { - ID: ServerProcessID, - LabelMajor: fixture.ServerName, - LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.ServerPID), - Rank: fixture.ServerName, - Shape: square, - Node: report.MakeNode(), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - ), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - nonContainerProcessID: { - ID: nonContainerProcessID, - LabelMajor: fixture.NonContainerName, - LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID), - Rank: fixture.NonContainerName, - Shape: square, - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - ), - EdgeMetadata: report.EdgeMetadata{}, - }, - unknownPseudoNode1ID: unknownPseudoNode1(ServerProcessID), - unknownPseudoNode2ID: unknownPseudoNode2(ServerProcessID), - render.IncomingInternetID: theIncomingInternetNode(ServerProcessID), + theOutgoingInternetNode = pseudo(render.OutgoingInternetID).WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.GoogleEndpointNodeID], + )) + + RenderedEndpoints = report.Nodes{ + fixture.Client54001NodeID: endpoint(fixture.Client54001NodeID, fixture.Server80NodeID), + fixture.Client54002NodeID: endpoint(fixture.Client54002NodeID, fixture.Server80NodeID), + fixture.Server80NodeID: endpoint(fixture.Server80NodeID), + fixture.UnknownClient1NodeID: endpoint(fixture.UnknownClient1NodeID, fixture.Server80NodeID), + fixture.UnknownClient2NodeID: endpoint(fixture.UnknownClient2NodeID, fixture.Server80NodeID), + fixture.UnknownClient3NodeID: endpoint(fixture.UnknownClient3NodeID, fixture.Server80NodeID), + fixture.RandomClientNodeID: endpoint(fixture.RandomClientNodeID, fixture.Server80NodeID), + fixture.NonContainerNodeID: endpoint(fixture.NonContainerNodeID, fixture.GoogleEndpointNodeID), + fixture.GoogleEndpointNodeID: endpoint(fixture.GoogleEndpointNodeID), + } + + RenderedProcesses = report.Nodes{ + fixture.ClientProcess1NodeID: processNode(fixture.ClientProcess1NodeID, fixture.ServerProcessNodeID). + WithLatests(map[string]string{ + report.HostNodeID: fixture.ClientHostNodeID, + process.PID: fixture.Client1PID, + process.Name: fixture.Client1Name, + }). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + )), + + fixture.ClientProcess2NodeID: processNode(fixture.ClientProcess2NodeID, fixture.ServerProcessNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54002NodeID], + )), + + fixture.ServerProcessNodeID: processNode(fixture.ServerProcessNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + )), + + fixture.NonContainerProcessNodeID: processNode(fixture.NonContainerProcessNodeID, render.OutgoingInternetID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.NonContainerNodeID], + )), + + UnknownPseudoNode1ID: unknownPseudoNode1(fixture.ServerProcessNodeID), + UnknownPseudoNode2ID: unknownPseudoNode2(fixture.ServerProcessNodeID), + + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerProcessNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - RenderedProcessNames = (render.RenderableNodes{ - fixture.Client1Name: { - ID: fixture.Client1Name, - LabelMajor: fixture.Client1Name, - LabelMinor: "2 processes", - Rank: fixture.Client1Name, - Shape: square, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - ), - Node: report.MakeNode().WithAdjacent(fixture.ServerName), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - }, - fixture.ServerName: { - ID: fixture.ServerName, - LabelMajor: fixture.ServerName, - LabelMinor: "1 process", - Rank: fixture.ServerName, - Shape: square, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - RenderedProcesses[ServerProcessID], - ), - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - fixture.NonContainerName: { - ID: fixture.NonContainerName, - LabelMajor: fixture.NonContainerName, - LabelMinor: "1 process", - Rank: fixture.NonContainerName, - Shape: square, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[nonContainerProcessID], - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{}, - }, - unknownPseudoNode1ID: unknownPseudoNode1(fixture.ServerName), - unknownPseudoNode2ID: unknownPseudoNode2(fixture.ServerName), + } + + RenderedProcessNames = report.Nodes{ + fixture.Client1Name: processNode(fixture.Client1Name, fixture.ServerName). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + )), + + fixture.ServerName: processNode(fixture.ServerName). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + )), + + fixture.NonContainerName: processNode(fixture.NonContainerName, render.OutgoingInternetID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.NonContainerNodeID], + RenderedProcesses[fixture.NonContainerProcessNodeID], + )), + + UnknownPseudoNode1ID: unknownPseudoNode1(fixture.ServerName), + UnknownPseudoNode2ID: unknownPseudoNode2(fixture.ServerName), render.IncomingInternetID: theIncomingInternetNode(fixture.ServerName), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - ClientContainerID = render.MakeContainerID(fixture.ClientContainerID) - ServerContainerID = render.MakeContainerID(fixture.ServerContainerID) - uncontainedServerID = render.MakePseudoNodeID(render.UncontainedID, fixture.ServerHostID) - - RenderedContainers = (render.RenderableNodes{ - ClientContainerID: { - ID: ClientContainerID, - LabelMajor: "client", - LabelMinor: fixture.ClientHostName, - Shape: hexagon, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - ), - Node: report.MakeNode().WithAdjacent(ServerContainerID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - ControlNode: fixture.ClientContainerNodeID, - }, - ServerContainerID: { - ID: ServerContainerID, - LabelMajor: "server", - LabelMinor: fixture.ServerHostName, - Shape: hexagon, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - RenderedProcesses[ServerProcessID], - ), - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - ControlNode: fixture.ServerContainerNodeID, - }, - uncontainedServerID: { - ID: uncontainedServerID, - LabelMajor: render.UncontainedMajor, - LabelMinor: fixture.ServerHostName, - Shape: square, - Stack: true, - Pseudo: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[nonContainerProcessID], - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{}, - }, - // unknownPseudoNode1ID: unknownPseudoNode1(ServerContainerID), - // unknownPseudoNode2ID: unknownPseudoNode2(ServerContainerID), - render.IncomingInternetID: theIncomingInternetNode(ServerContainerID), + } + + uncontainedServerID = render.MakePseudoNodeID(render.UncontainedID, fixture.ServerHostID) + uncontainedServerNode = pseudo(uncontainedServerID, render.OutgoingInternetID).WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.NonContainerNodeID], + RenderedProcesses[fixture.NonContainerProcessNodeID], + )) + + RenderedContainers = report.Nodes{ + fixture.ClientContainerNodeID: container(fixture.ClientContainerNodeID, fixture.ServerContainerNodeID). + WithLatests(map[string]string{ + report.HostNodeID: fixture.ClientHostNodeID, + docker.ContainerID: fixture.ClientContainerID, + docker.ContainerName: fixture.ClientContainerName, + docker.ImageName: fixture.ClientContainerImageName, + }). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + )), + + fixture.ServerContainerNodeID: container(fixture.ServerContainerNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + )), + + uncontainedServerID: uncontainedServerNode, + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerContainerNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - ClientContainerImageID = render.MakeContainerImageID(fixture.ClientContainerImageName) - ServerContainerImageID = render.MakeContainerImageID(fixture.ServerContainerImageName) - - RenderedContainerImages = (render.RenderableNodes{ - ClientContainerImageID: { - ID: ClientContainerImageID, - LabelMajor: fixture.ClientContainerImageName, - LabelMinor: "1 container", - Rank: fixture.ClientContainerImageName, - Shape: hexagon, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - RenderedContainers[ClientContainerID], - ), - Node: report.MakeNode().WithAdjacent(ServerContainerImageID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - }, - ServerContainerImageID: { - ID: ServerContainerImageID, - LabelMajor: fixture.ServerContainerImageName, - LabelMinor: "1 container", - Rank: fixture.ServerContainerImageName, - Shape: hexagon, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - RenderedProcesses[ServerProcessID], - RenderedContainers[ServerContainerID], - ), - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - uncontainedServerID: { - ID: uncontainedServerID, - LabelMajor: render.UncontainedMajor, - LabelMinor: fixture.ServerHostName, - Shape: square, - Stack: true, - Pseudo: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[nonContainerProcessID], - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{}, - }, - // unknownPseudoNode1ID: unknownPseudoNode1(ServerContainerImageID), - // unknownPseudoNode2ID: unknownPseudoNode2(ServerContainerImageID), - render.IncomingInternetID: theIncomingInternetNode(ServerContainerImageID), + } + + RenderedContainerImages = report.Nodes{ + fixture.ClientContainerImageNodeID: containerImage(fixture.ClientContainerImageNodeID, fixture.ServerContainerImageNodeID). + WithLatests(map[string]string{ + report.HostNodeID: fixture.ClientHostNodeID, + docker.ImageID: fixture.ClientContainerImageID, + docker.ImageName: fixture.ClientContainerImageName, + }). + WithCounters(map[string]int{ + report.Container: 1, + }). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + RenderedContainers[fixture.ClientContainerNodeID], + )), + + fixture.ServerContainerImageNodeID: containerImage(fixture.ServerContainerImageNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + RenderedContainers[fixture.ServerContainerNodeID], + )), + + uncontainedServerID: uncontainedServerNode, + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerContainerImageNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - ClientPodRenderedID = render.MakePodID("ping/pong-a") - ServerPodRenderedID = render.MakePodID("ping/pong-b") - - RenderedPods = (render.RenderableNodes{ - ClientPodRenderedID: { - ID: ClientPodRenderedID, - LabelMajor: "pong-a", - LabelMinor: "1 container", - Rank: "ping/pong-a", - Shape: heptagon, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - RenderedContainers[ClientContainerID], - ), - Node: report.MakeNode().WithAdjacent(ServerPodRenderedID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - }, - ServerPodRenderedID: { - ID: ServerPodRenderedID, - LabelMajor: "pong-b", - LabelMinor: "1 container", - Rank: "ping/pong-b", - Shape: heptagon, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - RenderedProcesses[ServerProcessID], - RenderedContainers[ServerContainerID], - ), - Node: report.MakeNode(), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - uncontainedServerID: { - ID: uncontainedServerID, - LabelMajor: render.UncontainedMajor, - LabelMinor: fixture.ServerHostName, - Pseudo: true, - Shape: square, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[nonContainerProcessID], - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{}, - }, - // unknownPseudoNode1ID: unknownPseudoNode1(ServerPodRenderedID), - // unknownPseudoNode2ID: unknownPseudoNode2(ServerPodRenderedID), - render.IncomingInternetID: theIncomingInternetNode(ServerPodRenderedID), + } + + RenderedHosts = report.Nodes{ + fixture.ClientHostNodeID: hostNode(fixture.ClientHostNodeID, fixture.ServerHostNodeID). + WithLatests(map[string]string{ + host.HostName: fixture.ClientHostName, + }). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + RenderedContainers[fixture.ClientContainerNodeID], + RenderedContainerImages[fixture.ClientContainerImageNodeID], + //RenderedPods[fixture.ClientPodNodeID], #1142 + )), + + fixture.ServerHostNodeID: hostNode(fixture.ServerHostNodeID, render.OutgoingInternetID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedEndpoints[fixture.NonContainerNodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + RenderedProcesses[fixture.NonContainerProcessNodeID], + RenderedContainers[fixture.ServerContainerNodeID], + RenderedContainerImages[fixture.ServerContainerImageNodeID], + //RenderedPods[fixture.ServerPodNodeID], #1142 + )), + + UnknownPseudoNode1ID: unknownPseudoNode1(fixture.ServerHostNodeID), + UnknownPseudoNode2ID: unknownPseudoNode2(fixture.ServerHostNodeID), + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerHostNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - ServerHostID = render.MakeHostID(fixture.ServerHostID) - ClientHostID = render.MakeHostID(fixture.ClientHostID) - - RenderedHosts = (render.RenderableNodes{ - ClientHostID: { - ID: ClientHostID, - LabelMajor: "client", // before first . - LabelMinor: "hostname.com", // after first . - Rank: "hostname.com", - Shape: circle, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - RenderedContainers[ClientContainerID], - RenderedContainerImages[ClientContainerImageID], - //RenderedPods[ClientPodRenderedID], #1142 - ), - Node: report.MakeNode().WithAdjacent(ServerHostID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - }, - }, - ServerHostID: { - ID: ServerHostID, - LabelMajor: "server", // before first . - LabelMinor: "hostname.com", // after first . - Rank: "hostname.com", - Shape: circle, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[ServerEndpointID], - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[ServerProcessID], - RenderedProcesses[nonContainerProcessID], - RenderedContainers[ServerContainerID], - RenderedContainerImages[ServerContainerImageID], - //RenderedPods[ServerPodRenderedID], #1142 - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{ - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - unknownPseudoNode1ID: unknownPseudoNode1(ServerHostID), - unknownPseudoNode2ID: unknownPseudoNode2(ServerHostID), - render.IncomingInternetID: theIncomingInternetNode(ServerHostID), + } + + RenderedPods = report.Nodes{ + fixture.ClientPodNodeID: pod(fixture.ClientPodNodeID, fixture.ServerPodNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + RenderedContainers[fixture.ClientContainerNodeID], + )), + + fixture.ServerPodNodeID: pod(fixture.ServerPodNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + RenderedContainers[fixture.ServerContainerNodeID], + )), + + uncontainedServerID: uncontainedServerNode, + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerPodNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() - - ServiceRenderedID = render.MakeServiceID("ping/pongservice") - - RenderedPodServices = (render.RenderableNodes{ - ServiceRenderedID: { - ID: ServiceRenderedID, - LabelMajor: "pongservice", - LabelMinor: "2 pods", - Rank: fixture.ServiceID, - Shape: heptagon, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[Client54001EndpointID], - RenderedEndpoints[Client54002EndpointID], - RenderedEndpoints[ServerEndpointID], - RenderedProcesses[ClientProcess1ID], - RenderedProcesses[ClientProcess2ID], - RenderedProcesses[ServerProcessID], - RenderedContainers[ClientContainerID], - RenderedContainers[ServerContainerID], - RenderedPods[ClientPodRenderedID], - RenderedPods[ServerPodRenderedID], - ), - Node: report.MakeNode().WithAdjacent(ServiceRenderedID), - EdgeMetadata: report.EdgeMetadata{ - EgressPacketCount: newu64(30), - EgressByteCount: newu64(300), - IngressPacketCount: newu64(210), - IngressByteCount: newu64(2100), - }, - }, - uncontainedServerID: { - ID: uncontainedServerID, - LabelMajor: render.UncontainedMajor, - LabelMinor: fixture.ServerHostName, - Pseudo: true, - Shape: square, - Stack: true, - Children: render.MakeRenderableNodeSet( - RenderedEndpoints[NonContainerEndpointID], - RenderedProcesses[nonContainerProcessID], - ), - Node: report.MakeNode().WithAdjacent(render.OutgoingInternetID), - EdgeMetadata: report.EdgeMetadata{}, - }, - // unknownPseudoNode1ID: unknownPseudoNode1(ServiceRenderedID), - // unknownPseudoNode2ID: unknownPseudoNode2(ServiceRenderedID), - render.IncomingInternetID: theIncomingInternetNode(ServiceRenderedID), + } + + RenderedPodServices = report.Nodes{ + fixture.ServiceNodeID: service(fixture.ServiceNodeID, fixture.ServiceNodeID). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Client54001NodeID], + RenderedEndpoints[fixture.Client54002NodeID], + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ClientProcess1NodeID], + RenderedProcesses[fixture.ClientProcess2NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + RenderedContainers[fixture.ClientContainerNodeID], + RenderedContainers[fixture.ServerContainerNodeID], + RenderedPods[fixture.ClientPodNodeID], + RenderedPods[fixture.ServerPodNodeID], + )), + + uncontainedServerID: uncontainedServerNode, + render.IncomingInternetID: theIncomingInternetNode(fixture.ServiceNodeID), render.OutgoingInternetID: theOutgoingInternetNode, - }).Prune() + } ) func newu64(value uint64) *uint64 { return &value } diff --git a/render/filters.go b/render/filters.go index d918000660..27ed285a40 100644 --- a/render/filters.go +++ b/render/filters.go @@ -1,6 +1,12 @@ package render import ( + "strings" + + "github.com/weaveworks/scope/common/mtime" + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/report" ) @@ -8,12 +14,12 @@ import ( // in one call - useful for functions that need to consider the entire graph. // We should minimise the use of this renderer type, as it is very inflexible. type CustomRenderer struct { - RenderFunc func(RenderableNodes) RenderableNodes + RenderFunc func(report.Nodes) report.Nodes Renderer } // Render implements Renderer -func (c CustomRenderer) Render(rpt report.Report) RenderableNodes { +func (c CustomRenderer) Render(rpt report.Report) report.Nodes { return c.RenderFunc(c.Renderer.Render(rpt)) } @@ -23,7 +29,7 @@ func (c CustomRenderer) Render(rpt report.Report) RenderableNodes { func ColorConnected(r Renderer) Renderer { return CustomRenderer{ Renderer: r, - RenderFunc: func(input RenderableNodes) RenderableNodes { + RenderFunc: func(input report.Nodes) report.Nodes { connected := map[string]struct{}{} void := struct{}{} @@ -42,9 +48,7 @@ func ColorConnected(r Renderer) Renderer { output := input.Copy() for id := range connected { - output[id] = output[id].WithNode(report.MakeNodeWith(map[string]string{ - IsConnected: "true", - })) + output[id] = output[id].WithLatest(IsConnected, mtime.Now(), "true") } return output }, @@ -54,22 +58,22 @@ func ColorConnected(r Renderer) Renderer { // Filter removes nodes from a view based on a predicate. type Filter struct { Renderer - FilterFunc func(RenderableNode) bool + FilterFunc func(report.Node) bool } // MakeFilter makes a new Filter. -func MakeFilter(f func(RenderableNode) bool, r Renderer) Renderer { +func MakeFilter(f func(report.Node) bool, r Renderer) Renderer { return Memoise(&Filter{r, f}) } // Render implements Renderer -func (f *Filter) Render(rpt report.Report) RenderableNodes { +func (f *Filter) Render(rpt report.Report) report.Nodes { nodes, _ := f.render(rpt) return nodes } -func (f *Filter) render(rpt report.Report) (RenderableNodes, int) { - output := RenderableNodes{} +func (f *Filter) render(rpt report.Report) (report.Nodes, int) { + output := report.Nodes{} inDegrees := map[string]int{} filtered := 0 for id, node := range f.Renderer.Render(rpt) { @@ -100,7 +104,7 @@ func (f *Filter) render(rpt report.Report) (RenderableNodes, int) { continue } node := output[id] - if !node.Pseudo || len(node.Adjacency) > 0 { + if node.Topology != Pseudo || len(node.Adjacency) > 0 { continue } delete(output, id) @@ -123,16 +127,16 @@ const IsConnected = "is_connected" // Complement takes a FilterFunc f and returns a FilterFunc that has the same // effects, if any, and returns the opposite truth value. -func Complement(f func(RenderableNode) bool) func(RenderableNode) bool { - return func(node RenderableNode) bool { return !f(node) } +func Complement(f func(report.Node) bool) func(report.Node) bool { + return func(node report.Node) bool { return !f(node) } } // FilterPseudo produces a renderer that removes pseudo nodes from the given // renderer func FilterPseudo(r Renderer) Renderer { return MakeFilter( - func(node RenderableNode) bool { - return !node.Pseudo + func(node report.Node) bool { + return node.Topology != Pseudo }, r, ) @@ -142,7 +146,7 @@ func FilterPseudo(r Renderer) Renderer { // from the given renderer func FilterUnconnected(r Renderer) Renderer { return MakeFilter( - func(node RenderableNode) bool { + func(node report.Node) bool { _, ok := node.Latest.Lookup(IsConnected) return ok }, @@ -157,22 +161,65 @@ func FilterNoop(in Renderer) Renderer { // FilterStopped filters out stopped containers. func FilterStopped(r Renderer) Renderer { - return MakeFilter(RenderableNode.IsStopped, r) + return MakeFilter(IsStopped, r) +} + +// IsStopped checks if the node is a stopped docker container +func IsStopped(n report.Node) bool { + containerState, ok := n.Latest.Lookup(docker.ContainerState) + return !ok || containerState != docker.StateStopped } // FilterRunning filters out running containers. func FilterRunning(r Renderer) Renderer { - return MakeFilter(Complement(RenderableNode.IsStopped), r) + return MakeFilter(Complement(IsStopped), r) +} + +// FilterNonProcspied removes endpoints which were not found in procspy. +func FilterNonProcspied(r Renderer) Renderer { + return MakeFilter( + func(node report.Node) bool { + _, ok := node.Latest.Lookup(endpoint.Procspied) + return ok + }, + r, + ) +} + +// IsSystem checks if the node is a "system" node +func IsSystem(n report.Node) bool { + containerName, _ := n.Latest.Lookup(docker.ContainerName) + if _, ok := systemContainerNames[containerName]; ok { + return false + } + imageName, _ := n.Latest.Lookup(docker.ImageName) + imagePrefix := strings.SplitN(imageName, ":", 2)[0] // :( + if _, ok := systemImagePrefixes[imagePrefix]; ok { + return false + } + roleLabel, _ := n.Latest.Lookup(docker.LabelPrefix + "works.weave.role") + if roleLabel == "system" { + return false + } + namespace, _ := n.Latest.Lookup(kubernetes.Namespace) + if namespace == "kube-system" { + return false + } + podName, _ := n.Latest.Lookup(docker.LabelPrefix + "io.kubernetes.pod.name") + if strings.HasPrefix(podName, "kube-system/") { + return false + } + return true } // FilterSystem is a Renderer which filters out system nodes. func FilterSystem(r Renderer) Renderer { - return MakeFilter(RenderableNode.IsSystem, r) + return MakeFilter(IsSystem, r) } // FilterApplication is a Renderer which filters out system nodes. func FilterApplication(r Renderer) Renderer { - return MakeFilter(Complement(RenderableNode.IsSystem), r) + return MakeFilter(Complement(IsSystem), r) } var systemContainerNames = map[string]struct{}{ diff --git a/render/filters_test.go b/render/filters_test.go index 70af0b88d0..e67e5fe9ab 100644 --- a/render/filters_test.go +++ b/render/filters_test.go @@ -11,16 +11,17 @@ import ( func TestFilterRender(t *testing.T) { renderer := render.FilterUnconnected( - mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("foo")}, - "baz": {ID: "baz", Node: report.MakeNode()}, + mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("bar"), + "bar": report.MakeNode().WithID("bar").WithAdjacent("foo"), + "baz": report.MakeNode().WithID("baz"), }}) - want := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("foo")}, + + have := report.MakeIDList() + for id := range renderer.Render(report.MakeReport()) { + have = have.Add(id) } - have := renderer.Render(report.MakeReport()).Prune() + want := report.MakeIDList("foo", "bar") if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -29,22 +30,19 @@ func TestFilterRender(t *testing.T) { func TestFilterRender2(t *testing.T) { // Test adjacencies are removed for filtered nodes. renderer := render.Filter{ - FilterFunc: func(node render.RenderableNode) bool { + FilterFunc: func(node report.Node) bool { return node.ID != "bar" }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("foo")}, - "baz": {ID: "baz", Node: report.MakeNode()}, + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("bar"), + "bar": report.MakeNode().WithID("bar").WithAdjacent("foo"), + "baz": report.MakeNode().WithID("baz"), }}, } - want := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, - "baz": {ID: "baz", Node: report.MakeNode()}, - } - have := renderer.Render(report.MakeReport()).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + + have := renderer.Render(report.MakeReport()) + if have["foo"].Adjacency.Contains("bar") { + t.Error("adjacencies for removed nodes should have been removed") } } @@ -52,59 +50,53 @@ func TestFilterUnconnectedPseudoNodes(t *testing.T) { // Test pseudo nodes that are made unconnected by filtering // are also removed. { - nodes := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("baz")}, - "baz": {ID: "baz", Node: report.MakeNode(), Pseudo: true}, + nodes := report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("bar"), + "bar": report.MakeNode().WithID("bar").WithAdjacent("baz"), + "baz": report.MakeNode().WithID("baz").WithTopology(render.Pseudo), } renderer := render.Filter{ - FilterFunc: func(node render.RenderableNode) bool { + FilterFunc: func(node report.Node) bool { return true }, - Renderer: mockRenderer{RenderableNodes: nodes}, + Renderer: mockRenderer{Nodes: nodes}, } - want := nodes.Prune() - have := renderer.Render(report.MakeReport()).Prune() + want := nodes + have := renderer.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } } { renderer := render.Filter{ - FilterFunc: func(node render.RenderableNode) bool { + FilterFunc: func(node report.Node) bool { return node.ID != "bar" }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("baz")}, - "baz": {ID: "baz", Node: report.MakeNode(), Pseudo: true}, + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("bar"), + "bar": report.MakeNode().WithID("bar").WithAdjacent("baz"), + "baz": report.MakeNode().WithID("baz").WithTopology(render.Pseudo), }}, } - want := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, - } - have := renderer.Render(report.MakeReport()).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + have := renderer.Render(report.MakeReport()) + if _, ok := have["baz"]; ok { + t.Error("expected the unconnected pseudonode baz to have been removed") } } { renderer := render.Filter{ - FilterFunc: func(node render.RenderableNode) bool { + FilterFunc: func(node report.Node) bool { return node.ID != "bar" }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, - "bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("foo")}, - "baz": {ID: "baz", Node: report.MakeNode().WithAdjacent("bar"), Pseudo: true}, + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo"), + "bar": report.MakeNode().WithID("bar").WithAdjacent("foo"), + "baz": report.MakeNode().WithID("baz").WithTopology(render.Pseudo).WithAdjacent("bar"), }}, } - want := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, - } - have := renderer.Render(report.MakeReport()).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + have := renderer.Render(report.MakeReport()) + if _, ok := have["baz"]; ok { + t.Error("expected the unconnected pseudonode baz to have been removed") } } } @@ -112,14 +104,13 @@ func TestFilterUnconnectedPseudoNodes(t *testing.T) { func TestFilterUnconnectedSelf(t *testing.T) { // Test nodes that are only connected to themselves are filtered. { - nodes := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("foo")}, + nodes := report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("foo"), } - renderer := render.FilterUnconnected(mockRenderer{RenderableNodes: nodes}) - want := render.RenderableNodes{} - have := renderer.Render(report.MakeReport()).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + renderer := render.FilterUnconnected(mockRenderer{Nodes: nodes}) + have := renderer.Render(report.MakeReport()) + if len(have) > 0 { + t.Error("expected node only connected to self to be removed") } } } @@ -127,17 +118,14 @@ func TestFilterUnconnectedSelf(t *testing.T) { func TestFilterPseudo(t *testing.T) { // Test pseudonodes are removed { - nodes := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, - "bar": {ID: "bar", Pseudo: true, Node: report.MakeNode()}, - } - renderer := render.FilterPseudo(mockRenderer{RenderableNodes: nodes}) - want := render.RenderableNodes{ - "foo": {ID: "foo", Node: report.MakeNode()}, + nodes := report.Nodes{ + "foo": report.MakeNode().WithID("foo"), + "bar": report.MakeNode().WithID("bar").WithTopology(render.Pseudo), } - have := renderer.Render(report.MakeReport()).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + renderer := render.FilterPseudo(mockRenderer{Nodes: nodes}) + have := renderer.Render(report.MakeReport()) + if _, ok := have["bar"]; ok { + t.Error("expected pseudonode to be removed") } } } diff --git a/render/id.go b/render/id.go index 47e98ec9b5..0ed8d49975 100644 --- a/render/id.go +++ b/render/id.go @@ -4,58 +4,7 @@ import ( "strings" ) -// ParseEndpointID parses endpoint IDs -func ParseEndpointID(id string) (host, ip, port string, ok bool) { - parts := strings.SplitN(id, ":", 4) - if len(parts) != 4 || parts[0] != "endpoint" { - return - } - host, ip, port, ok = parts[1], parts[2], parts[3], true - return -} - -// makeID is the generic ID maker -func makeID(prefix string, parts ...string) string { - return strings.Join(append([]string{prefix}, parts...), ":") -} - -// MakeEndpointID makes an endpoint node ID for rendered nodes. -func MakeEndpointID(hostID, addr, port string) string { - return makeID("endpoint", hostID, addr, port) -} - -// MakeProcessID makes a process node ID for rendered nodes. -func MakeProcessID(hostID, pid string) string { - return makeID("process", hostID, pid) -} - -// MakeContainerID makes a container node ID for rendered nodes. -func MakeContainerID(containerID string) string { - return makeID("container", containerID) -} - -// MakeContainerImageID makes a container image node ID for rendered nodes. -func MakeContainerImageID(imageID string) string { - return makeID("container_image", imageID) -} - -// MakePodID makes a pod node ID for rendered nodes. -func MakePodID(podID string) string { - return makeID("pod", podID) -} - -// MakeServiceID makes a service node ID for rendered nodes. -func MakeServiceID(serviceID string) string { - return makeID("service", serviceID) -} - -// MakeHostID makes a host node ID for rendered nodes. -func MakeHostID(hostID string) string { - return makeID("host", hostID) -} - -// MakePseudoNodeID produces a pseudo node ID from its composite parts, -// for use in rendered nodes. +// MakePseudoNodeID joins the parts of an id into the id of a pseudonode func MakePseudoNodeID(parts ...string) string { - return makeID("pseudo", parts...) + return strings.Join(append([]string{"pseudo"}, parts...), ":") } diff --git a/render/mapping.go b/render/mapping.go index a449256fd8..ea1605bfe2 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -8,7 +8,6 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" - "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" @@ -27,229 +26,53 @@ const ( InboundMinor = "Inbound connections" OutboundMinor = "Outbound connections" - ContainersKey = "containers" - ipsKey = "ips" - podsKey = "pods" - processesKey = "processes" - servicesKey = "services" + ipsKey = "ips" - AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name" - KubernetesContainerNameLabel = "io.kubernetes.container.name" + // Topology for pseudo-nodes and IPs so we can differentiate them at the end + Pseudo = "pseudo" + IP = "IP" ) -// MapFunc is anything which can take an arbitrary RenderableNode and -// return a set of other RenderableNodes. +// MapFunc is anything which can take an arbitrary Node and +// return a set of other Nodes. // // If the output is empty, the node shall be omitted from the rendered topology. -type MapFunc func(RenderableNode, report.Networks) RenderableNodes +type MapFunc func(report.Node, report.Networks) report.Nodes -func theInternetNode(m RenderableNode) RenderableNode { - node := newDerivedPseudoNode("", "", m) - node.Shape = Cloud - // emit one internet node for incoming, one for outgoing - if len(m.Adjacency) > 0 { - node.ID = IncomingInternetID - node.LabelMajor = InboundMajor - node.LabelMinor = InboundMinor - } else { - node.ID = OutgoingInternetID - node.LabelMajor = OutboundMajor - node.LabelMinor = OutboundMinor - } - return node -} - -// MapEndpointIdentity remaps endpoints to have an id format consistent -// with render/id.go; no pseudo nodes are introduced in this step, so -// that pseudo nodes introduces later are guaranteed to have endpoints -// as children. This is needed to construct the connection details tables. -func MapEndpointIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - addr, ok := m.Latest.Lookup(endpoint.Addr) - if !ok { - return RenderableNodes{} - } - - port, ok := m.Latest.Lookup(endpoint.Port) - if !ok { - return RenderableNodes{} - } - - // We only show nodes found through procspy in this view. - _, procspied := m.Latest.Lookup(endpoint.Procspied) - if !procspied { - return RenderableNodes{} - } - - id := MakeEndpointID(report.ExtractHostID(m.Node), addr, port) - return RenderableNodes{id: NewRenderableNodeWith(id, "", "", "", m)} -} - -// MapProcessIdentity maps a process topology node to a process renderable -// node. As it is only ever run on process topology nodes, we expect that -// certain keys are present. -func MapProcessIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - pid, ok := m.Latest.Lookup(process.PID) - if !ok { - return RenderableNodes{} - } - - var ( - id = MakeProcessID(report.ExtractHostID(m.Node), pid) - major, _ = m.Latest.Lookup(process.Name) - minor = fmt.Sprintf("%s (%s)", report.ExtractHostID(m.Node), pid) - rank, _ = m.Latest.Lookup(process.Name) - ) - - node := NewRenderableNodeWith(id, major, minor, rank, m) - node.Shape = Square - return RenderableNodes{id: node} +// NewDerivedNode makes a node based on node, but with a new ID +func NewDerivedNode(id string, node report.Node) report.Node { + return node.WithID(id).WithChildren(report.MakeNodeSet(node)).PruneParents() } -// MapContainerIdentity maps a container topology node to a container -// renderable node. As it is only ever run on container topology nodes, we -// expect that certain keys are present. -func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - containerID, ok := m.Latest.Lookup(docker.ContainerID) - if !ok { - return RenderableNodes{} - } - - var ( - id = MakeContainerID(containerID) - major, _ = GetRenderableContainerName(m.Node) - minor = report.ExtractHostID(m.Node) - ) - - node := NewRenderableNodeWith(id, major, minor, "", m) - node.ControlNode = m.ID - node.Shape = Hexagon - return RenderableNodes{id: node} -} - -// GetRenderableContainerName obtains a user-friendly container name, to render in the UI -func GetRenderableContainerName(nmd report.Node) (string, bool) { - // Amazon's ecs-agent produces huge Docker container names, destructively - // derived from mangling Container Definition names in Task - // Definitions. - // - // However, the ecs-agent provides a label containing the original Container - // Definition name. - if labelValue, ok := nmd.Latest.Lookup(docker.LabelPrefix + AmazonECSContainerNameLabel); ok { - return labelValue, true - } - - // Kubernetes also mangles its Docker container names and provides a - // label with the original container name. However, note that this label - // is only provided by Kubernetes versions >= 1.2 (see - // https://github.com/kubernetes/kubernetes/pull/17234/ ) - if labelValue, ok := nmd.Latest.Lookup(docker.LabelPrefix + KubernetesContainerNameLabel); ok { - return labelValue, true - } - - name, ok := nmd.Latest.Lookup(docker.ContainerName) - return name, ok -} - -// MapContainerImageIdentity maps a container image topology node to container -// image renderable node. As it is only ever run on container image topology -// nodes, we expect that certain keys are present. -func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - imageID, ok := m.Latest.Lookup(docker.ImageID) - if !ok { - return RenderableNodes{} - } - - var ( - id = MakeContainerImageID(imageID) - major, _ = m.Latest.Lookup(docker.ImageName) - rank = imageID - ) - - node := NewRenderableNodeWith(id, major, "", rank, m) - node.Shape = Hexagon - node.Stack = true - return RenderableNodes{id: node} -} - -// MapPodIdentity maps a pod topology node to pod renderable node. As it is -// only ever run on pod topology nodes, we expect that certain keys -// are present. -func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - podID, ok := m.Latest.Lookup(kubernetes.PodID) - if !ok { - return RenderableNodes{} - } - - var ( - id = MakePodID(podID) - major, _ = m.Latest.Lookup(kubernetes.PodName) - rank, _ = m.Latest.Lookup(kubernetes.PodID) - ) - - node := NewRenderableNodeWith(id, major, "", rank, m) - node.Shape = Heptagon - return RenderableNodes{id: node} -} - -// MapServiceIdentity maps a service topology node to service renderable node. As it is -// only ever run on service topology nodes, we expect that certain keys -// are present. -func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - serviceID, ok := m.Latest.Lookup(kubernetes.ServiceID) - if !ok { - return RenderableNodes{} - } - - var ( - id = MakeServiceID(serviceID) - major, _ = m.Latest.Lookup(kubernetes.ServiceName) - rank, _ = m.Latest.Lookup(kubernetes.ServiceID) - ) - - node := NewRenderableNodeWith(id, major, "", rank, m) - node.Shape = Heptagon - node.Stack = true - return RenderableNodes{id: node} +// NewDerivedPseudoNode makes a new pseudo node with the node as a child +func NewDerivedPseudoNode(id string, node report.Node) report.Node { + return node.WithID(id).WithTopology(Pseudo).WithChildren(report.MakeNodeSet(node)).PruneParents() } -// MapHostIdentity maps a host topology node to a host renderable node. As it -// is only ever run on host topology nodes, we expect that certain keys are -// present. -func MapHostIdentity(m RenderableNode, _ report.Networks) RenderableNodes { - var ( - id = MakeHostID(report.ExtractHostID(m.Node)) - hostname, _ = m.Latest.Lookup(host.HostName) - parts = strings.SplitN(hostname, ".", 2) - major, minor, rank = "", "", "" - ) - - if len(parts) == 2 { - major, minor, rank = parts[0], parts[1], parts[1] - } else { - major = hostname +func theInternetNode(m report.Node) report.Node { + // emit one internet node for incoming, one for outgoing + if len(m.Adjacency) > 0 { + return NewDerivedPseudoNode(IncomingInternetID, m) } - - node := NewRenderableNodeWith(id, major, minor, rank, m) - node.Shape = Circle - return RenderableNodes{id: node} + return NewDerivedPseudoNode(OutgoingInternetID, m) } // MapEndpoint2IP maps endpoint nodes to their IP address, for joining // with container nodes. We drop endpoint nodes with pids, as they // will be joined to containers through the process topology, and we // don't want to double count edges. -func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes { +func MapEndpoint2IP(m report.Node, local report.Networks) report.Nodes { // Don't include procspied connections, to prevent double counting _, ok := m.Latest.Lookup(endpoint.Procspied) if ok { - return RenderableNodes{} + return report.Nodes{} } scope, addr, port, ok := report.ParseEndpointNodeID(m.ID) if !ok { - return RenderableNodes{} + return report.Nodes{} } if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { - return RenderableNodes{TheInternetID: theInternetNode(m)} + return report.Nodes{TheInternetID: theInternetNode(m)} } // We don't always know what port a container is listening on, and @@ -259,10 +82,9 @@ func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes { // So we need to emit two nodes, for two different cases. id := report.MakeScopedEndpointNodeID(scope, addr, "") idWithPort := report.MakeScopedEndpointNodeID(scope, addr, port) - m = m.WithParents(report.EmptySets) - return RenderableNodes{ - id: NewRenderableNodeWith(id, "", "", "", m), - idWithPort: NewRenderableNodeWith(idWithPort, "", "", "", m), + return report.Nodes{ + id: NewDerivedNode(id, m).WithTopology(IP), + idWithPort: NewDerivedNode(idWithPort, m).WithTopology(IP), } } @@ -271,8 +93,13 @@ var portMappingMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\. // MapContainer2IP maps container nodes to their IP addresses (outputs // multiple nodes). This allows container to be joined directly with // the endpoint topology. -func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes { - result := RenderableNodes{} +func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes { + containerID, ok := m.Latest.Lookup(docker.ContainerID) + if !ok { + return report.Nodes{} + } + + result := report.Nodes{} if addrs, ok := m.Sets.Lookup(docker.ContainerIPsWithScopes); ok { for _, addr := range addrs { scope, addr, ok := report.ParseAddressNodeID(addr) @@ -280,9 +107,11 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes { continue } id := report.MakeScopedEndpointNodeID(scope, addr, "") - node := NewRenderableNodeWith(id, "", "", "", m) - node.Counters = node.Counters.Add(ipsKey, 1) - result[id] = node + result[id] = NewDerivedNode(id, m). + WithTopology(IP). + WithLatests(map[string]string{docker.ContainerID: containerID}). + WithCounters(map[string]int{ipsKey: 1}) + } } @@ -293,9 +122,11 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes { if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil { ip, port := mapping[1], mapping[2] id := report.MakeScopedEndpointNodeID("", ip, port) - node := NewRenderableNodeWith(id, "", "", "", m.WithParents(report.EmptySets)) - node.Counters = node.Counters.Add(ipsKey, 1) - result[id] = node + result[id] = NewDerivedNode(id, m). + WithTopology(IP). + WithLatests(map[string]string{docker.ContainerID: containerID}). + WithCounters(map[string]int{ipsKey: 1}) + } } @@ -305,57 +136,54 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes { // MapIP2Container maps IP nodes produced from MapContainer2IP back to // container nodes. If there is more than one container with a given // IP, it is dropped. -func MapIP2Container(n RenderableNode, _ report.Networks) RenderableNodes { +func MapIP2Container(n report.Node, _ report.Networks) report.Nodes { // If an IP is shared between multiple containers, we can't // reliably attribute an connection based on its IP - if count, _ := n.Node.Counters.Lookup(ipsKey); count > 1 { - return RenderableNodes{} + if count, _ := n.Counters.Lookup(ipsKey); count > 1 { + return report.Nodes{} } // Propagate the internet pseudo node if strings.HasSuffix(n.ID, TheInternetID) { - return RenderableNodes{n.ID: n} + return report.Nodes{n.ID: n} } // If this node is not a container, exclude it. // This excludes all the nodes we've dragged in from endpoint // that we failed to join to a container. - containerID, ok := n.Node.Latest.Lookup(docker.ContainerID) + containerID, ok := n.Latest.Lookup(docker.ContainerID) if !ok { - return RenderableNodes{} + return report.Nodes{} } - id := MakeContainerID(containerID) - node := NewDerivedNode(id, n.WithParents(report.EmptySets)) - node.Shape = Hexagon - return RenderableNodes{id: node} + id := report.MakeContainerNodeID(containerID) + return report.Nodes{ + id: NewDerivedNode(id, n). + WithTopology(report.Container), + } } // MapEndpoint2Pseudo makes internet of host pesudo nodes from a endpoint node. -func MapEndpoint2Pseudo(n RenderableNode, local report.Networks) RenderableNodes { - var node RenderableNode +func MapEndpoint2Pseudo(n report.Node, local report.Networks) report.Nodes { + var node report.Node addr, ok := n.Latest.Lookup(endpoint.Addr) if !ok { - return RenderableNodes{} + return report.Nodes{} } if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { // If the dstNodeAddr is not in a network local to this report, we emit an // internet node node = theInternetNode(n) - } else { - - node = newDerivedPseudoNode(MakePseudoNodeID(addr), addr, n) + node = NewDerivedPseudoNode(MakePseudoNodeID(addr), n) } - - node.Children = node.Children.Add(n) - return RenderableNodes{node.ID: node} + return report.Nodes{node.ID: node} } -// MapEndpoint2Process maps endpoint RenderableNodes to process -// RenderableNodes. +// MapEndpoint2Process maps endpoint Nodes to process +// Nodes. // // 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 @@ -365,26 +193,26 @@ func MapEndpoint2Pseudo(n RenderableNode, local report.Networks) RenderableNodes // 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, local report.Networks) RenderableNodes { +func MapEndpoint2Process(n report.Node, local report.Networks) report.Nodes { // Nodes without a hostid are treated as pseudo nodes if _, ok := n.Latest.Lookup(report.HostNodeID); !ok { return MapEndpoint2Pseudo(n, local) } - pid, ok := n.Node.Latest.Lookup(process.PID) + pid, timestamp, ok := n.Latest.LookupEntry(process.PID) if !ok { - return RenderableNodes{} + return report.Nodes{} } - id := MakeProcessID(report.ExtractHostID(n.Node), pid) - node := NewDerivedNode(id, n.WithParents(report.EmptySets)) - node.Shape = Square - node.Children = node.Children.Add(n) - return RenderableNodes{id: node} + id := report.MakeProcessNodeID(report.ExtractHostID(n), pid) + node := NewDerivedNode(id, n).WithTopology(report.Process) + node.Latest = node.Latest.Set(process.PID, timestamp, pid) + node.Counters = node.Counters.Add(n.Topology, 1) + return report.Nodes{id: node} } -// MapProcess2Container maps process RenderableNodes to container -// RenderableNodes. +// MapProcess2Container maps process Nodes to container +// Nodes. // // If this function is given a node without a docker_container_id // (including other pseudo nodes), it will produce an "Uncontained" @@ -394,15 +222,15 @@ func MapEndpoint2Process(n RenderableNode, local report.Networks) RenderableNode // 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, _ report.Networks) RenderableNodes { +func MapProcess2Container(n report.Node, _ report.Networks) report.Nodes { // Propagate the internet pseudo node if strings.HasSuffix(n.ID, TheInternetID) { - return RenderableNodes{n.ID: n} + return report.Nodes{n.ID: n} } // Don't propagate non-internet pseudo nodes - if n.Pseudo { - return RenderableNodes{} + if n.Topology == Pseudo { + return report.Nodes{} } // Otherwise, if the process is not in a container, group it @@ -410,77 +238,42 @@ func MapProcess2Container(n RenderableNode, _ report.Networks) RenderableNodes { // this node doesn't have a host id in their nodemetadata, it'll // all get grouped into a single uncontained node. var ( - id string - node RenderableNode - hostID = report.ExtractHostID(n.Node) + id string + node report.Node ) - n = n.WithParents(report.EmptySets) - if containerID, ok := n.Node.Latest.Lookup(docker.ContainerID); ok { - id = MakeContainerID(containerID) - node = NewDerivedNode(id, n) - node.Shape = Hexagon + if containerID, ok := n.Latest.Lookup(docker.ContainerID); ok { + id = report.MakeContainerNodeID(containerID) + node = NewDerivedNode(id, n).WithTopology(report.Container) } else { - nCopy := n.Copy() - nCopy.Node = nCopy.Node.WithID("").WithTopology("") // Wipe the ID so it cannot be rendered. - id = MakePseudoNodeID(UncontainedID, hostID) - node = newDerivedPseudoNode(id, UncontainedMajor, nCopy) - node.LabelMinor = hostID - node.Shape = Square - node.Stack = true + id = MakePseudoNodeID(UncontainedID, report.ExtractHostID(n)) + node = NewDerivedPseudoNode(id, n) } - - node.Children = node.Children.Add(n) - return RenderableNodes{id: node} + return report.Nodes{id: node} } -// MapProcess2Name maps process RenderableNodes to RenderableNodes +// MapProcess2Name maps process Nodes to Nodes // 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, _ report.Networks) RenderableNodes { - if n.Pseudo { - return RenderableNodes{n.ID: n} +// is not to join the information with another topology. +func MapProcess2Name(n report.Node, _ report.Networks) report.Nodes { + if n.Topology == Pseudo { + return report.Nodes{n.ID: n} } - name, ok := n.Node.Latest.Lookup(process.Name) + name, timestamp, ok := n.Latest.LookupEntry(process.Name) if !ok { - return RenderableNodes{} - } - - node := NewDerivedNode(name, n) - node.LabelMajor = name - node.Rank = name - node.Counters = node.Node.Counters.Add(processesKey, 1) - node.Node.Topology = "process_name" - node.Node.ID = name - node.Children = node.Children.Add(n) - node.Shape = Square - node.Stack = true - return RenderableNodes{name: node} -} - -// MapCountProcessName maps 1:1 process name nodes, counting -// the number of processes grouped together and putting -// that info in the minor label. -func MapCountProcessName(n RenderableNode, _ report.Networks) RenderableNodes { - if n.Pseudo { - return RenderableNodes{n.ID: n} + return report.Nodes{} } - output := n.Copy() - processes, _ := n.Node.Counters.Lookup(processesKey) - if processes == 1 { - output.LabelMinor = "1 process" - } else { - output.LabelMinor = fmt.Sprintf("%d processes", processes) - } - return RenderableNodes{output.ID: output} + node := NewDerivedNode(name, n).WithTopology(report.Process) + node.Latest = node.Latest.Set(process.Name, timestamp, name) + node.Counters = node.Counters.Add(n.Topology, 1) + return report.Nodes{name: node} } -// MapContainer2ContainerImage maps container RenderableNodes to container -// image RenderableNodes. +// MapContainer2ContainerImage maps container Nodes to container +// image Nodes. // // If this function is given a node without a docker_image_id // (including other pseudo nodes), it will produce an "Uncontained" @@ -490,32 +283,25 @@ func MapCountProcessName(n RenderableNode, _ report.Networks) RenderableNodes { // 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 MapContainer2ContainerImage(n RenderableNode, _ report.Networks) RenderableNodes { +func MapContainer2ContainerImage(n report.Node, _ report.Networks) report.Nodes { // Propagate all pseudo nodes - if n.Pseudo { - return RenderableNodes{n.ID: n} + if n.Topology == Pseudo { + return report.Nodes{n.ID: n} } // Otherwise, if some some reason the container doesn't have a image_id // (maybe slightly out of sync reports), just drop it - imageID, ok := n.Node.Latest.Lookup(docker.ImageID) + imageID, timestamp, ok := n.Latest.LookupEntry(docker.ImageID) if !ok { - return RenderableNodes{} + return report.Nodes{} } // Add container id key to the counters, which will later be counted to produce the minor label - id := MakeContainerImageID(imageID) - result := NewDerivedNode(id, n.WithParents(report.EmptySets)) - result.Node.Counters = result.Node.Counters.Add(ContainersKey, 1) - - // Add the container as a child of the new image node - result.Children = result.Children.Add(n) - - result.Node.Topology = "container_image" - result.Node.ID = report.MakeContainerImageNodeID(imageID) - result.Shape = Hexagon - result.Stack = true - return RenderableNodes{id: result} + id := report.MakeContainerImageNodeID(imageID) + result := NewDerivedNode(id, n).WithTopology(report.ContainerImage) + result.Latest = result.Latest.Set(docker.ImageID, timestamp, imageID) + result.Counters = result.Counters.Add(n.Topology, 1) + return report.Nodes{id: result} } // ImageNameWithoutVersion splits the image name apart, returning the name @@ -529,36 +315,8 @@ func ImageNameWithoutVersion(name string) string { return parts[0] } -// MapContainerImage2Name maps container images RenderableNodes to -// RenderableNodes for each container image 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 MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes { - if n.Pseudo { - return RenderableNodes{n.ID: n} - } - - name, ok := n.Node.Latest.Lookup(docker.ImageName) - if !ok { - return RenderableNodes{} - } - - name = ImageNameWithoutVersion(name) - id := MakeContainerImageID(name) - - node := NewDerivedNode(id, n) - node.LabelMajor = name - node.Rank = name - node.Node = n.Node.Copy() // Propagate NMD for container counting. - node.Shape = Hexagon - node.Stack = true - return RenderableNodes{id: node} -} - -// MapX2Host maps any RenderableNodes to host -// RenderableNodes. +// MapX2Host maps any Nodes to host +// Nodes. // // If this function is given a node without a hostname // (including other pseudo nodes), it will drop the node. @@ -567,39 +325,40 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes // 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 MapX2Host(n RenderableNode, _ report.Networks) RenderableNodes { +func MapX2Host(n report.Node, _ report.Networks) report.Nodes { // Don't propagate all pseudo nodes - we do this in MapEndpoint2Host - if n.Pseudo { - return RenderableNodes{} - } - if _, ok := n.Node.Latest.Lookup(report.HostNodeID); !ok { - return RenderableNodes{} - } - id := MakeHostID(report.ExtractHostID(n.Node)) - result := NewDerivedNode(id, n.WithParents(report.EmptySets)) - result.Children = result.Children.Add(n) - result.Shape = Circle - result.EdgeMetadata = report.EdgeMetadata{} // Don't double count edge metadata - return RenderableNodes{id: result} + if n.Topology == Pseudo { + return report.Nodes{} + } + hostNodeID, timestamp, ok := n.Latest.LookupEntry(report.HostNodeID) + if !ok { + return report.Nodes{} + } + id := report.MakeHostNodeID(report.ExtractHostID(n)) + result := NewDerivedNode(id, n).WithTopology(report.Host) + result.Latest = result.Latest.Set(report.HostNodeID, timestamp, hostNodeID) + result.Counters = result.Counters.Add(n.Topology, 1) + return report.Nodes{id: result} } // MapEndpoint2Host takes nodes from the endpoint topology and produces // host nodes or pseudo nodes. -func MapEndpoint2Host(n RenderableNode, local report.Networks) RenderableNodes { +func MapEndpoint2Host(n report.Node, local report.Networks) report.Nodes { // Nodes without a hostid are treated as pseudo nodes - if _, ok := n.Latest.Lookup(report.HostNodeID); !ok { + hostNodeID, timestamp, ok := n.Latest.LookupEntry(report.HostNodeID) + if !ok { return MapEndpoint2Pseudo(n, local) } - id := MakeHostID(report.ExtractHostID(n.Node)) - result := NewDerivedNode(id, n.WithParents(report.EmptySets)) - result.Children = result.Children.Add(n) - result.Shape = Circle - return RenderableNodes{id: result} + id := report.MakeHostNodeID(report.ExtractHostID(n)) + result := NewDerivedNode(id, n).WithTopology(report.Host) + result.Latest = result.Latest.Set(report.HostNodeID, timestamp, hostNodeID) + result.Counters = result.Counters.Add(n.Topology, 1) + return report.Nodes{id: result} } -// MapContainer2Pod maps container RenderableNodes to pod -// RenderableNodes. +// MapContainer2Pod maps container Nodes to pod +// Nodes. // // If this function is given a node without a kubernetes_pod_id // (including other pseudo nodes), it will produce an "Unmanaged" @@ -609,42 +368,39 @@ func MapEndpoint2Host(n RenderableNode, local report.Networks) RenderableNodes { // 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 MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes { +func MapContainer2Pod(n report.Node, _ report.Networks) report.Nodes { // Propagate all pseudo nodes - if n.Pseudo { - return RenderableNodes{n.ID: n} + if n.Topology == Pseudo { + return report.Nodes{n.ID: n} } // Otherwise, if some some reason the container doesn't have a pod_id (maybe // slightly out of sync reports, or its not in a pod), just drop it - podID, ok := n.Node.Latest.Lookup(kubernetes.PodID) + namespace, ok := n.Latest.Lookup(kubernetes.Namespace) if !ok { - return RenderableNodes{} + return report.Nodes{} } - id := MakePodID(podID) - - // Add container- key to NMD, which will later be counted to produce the - // minor label - result := NewRenderableNodeWith(id, "", "", podID, n.WithParents(report.EmptySets)) - result.Counters = result.Counters.Add(ContainersKey, 1) + podID, ok := n.Latest.Lookup(kubernetes.PodID) + if !ok { + return report.Nodes{} + } + podName := strings.TrimPrefix(podID, namespace+"/") + id := report.MakePodNodeID(namespace, podName) // Due to a bug in kubernetes, addon pods on the master node are not returned - // from the API. This is a workaround until + // from the API. Adding the namespace and pod name is a workaround until // https://github.com/kubernetes/kubernetes/issues/14738 is fixed. - if s := strings.SplitN(podID, "/", 2); len(s) == 2 { - result.LabelMajor = s[1] - result.Node = result.Node.WithLatests(map[string]string{ - kubernetes.Namespace: s[0], - kubernetes.PodName: s[1], - }) + return report.Nodes{ + id: NewDerivedNode(id, n). + WithTopology(report.Pod). + WithLatests(map[string]string{ + kubernetes.Namespace: namespace, + kubernetes.PodName: podName, + }), } - - result.Shape = Heptagon - result.Children = result.Children.Add(n) - return RenderableNodes{id: result} } -// MapPod2Service maps pod RenderableNodes to service RenderableNodes. +// MapPod2Service maps pod Nodes to service Nodes. // // If this function is given a node without a kubernetes_pod_id // (including other pseudo nodes), it will produce an "Uncontained" @@ -654,91 +410,50 @@ func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes { // 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 pod graph to get that info. -func MapPod2Service(pod RenderableNode, _ report.Networks) RenderableNodes { +func MapPod2Service(pod report.Node, _ report.Networks) report.Nodes { // Propagate all pseudo nodes - if pod.Pseudo { - return RenderableNodes{pod.ID: pod} + if pod.Topology == Pseudo { + return report.Nodes{pod.ID: pod} } // Otherwise, if some some reason the pod doesn't have a service_ids (maybe // slightly out of sync reports, or its not in a service), just drop it - ids, ok := pod.Node.Latest.Lookup(kubernetes.ServiceIDs) + namespace, ok := pod.Latest.Lookup(kubernetes.Namespace) if !ok { - return RenderableNodes{} + return report.Nodes{} + } + ids, ok := pod.Latest.Lookup(kubernetes.ServiceIDs) + if !ok { + return report.Nodes{} } - result := RenderableNodes{} + result := report.Nodes{} for _, serviceID := range strings.Fields(ids) { - id := MakeServiceID(serviceID) - node := NewDerivedNode(id, pod.WithParents(report.EmptySets)) - node.Node.Counters = node.Node.Counters.Add(podsKey, 1) - node.Children = node.Children.Add(pod) - node.Shape = Heptagon - node.Stack = true - result[id] = node + serviceName := strings.TrimPrefix(serviceID, namespace+"/") + id := report.MakeServiceNodeID(namespace, serviceName) + result[id] = NewDerivedNode(id, pod).WithTopology(report.Service) } return result } -// MapContainer2Hostname maps container RenderableNodes to 'hostname' renderabled nodes.. -func MapContainer2Hostname(n RenderableNode, _ report.Networks) RenderableNodes { +// MapContainer2Hostname maps container Nodes to 'hostname' renderabled nodes.. +func MapContainer2Hostname(n report.Node, _ report.Networks) report.Nodes { // Propagate all pseudo nodes - if n.Pseudo { - return RenderableNodes{n.ID: n} + if n.Topology == Pseudo { + return report.Nodes{n.ID: n} } // Otherwise, if some some reason the container doesn't have a hostname // (maybe slightly out of sync reports), just drop it - id, ok := n.Node.Latest.Lookup(docker.ContainerHostname) + id, timestamp, ok := n.Latest.LookupEntry(docker.ContainerHostname) if !ok { - return RenderableNodes{} + return report.Nodes{} } - result := NewDerivedNode(id, n) - result.LabelMajor = id - result.Rank = id - - // Add container id key to the counters, which will later be counted to produce the minor label - result.Counters = result.Counters.Add(ContainersKey, 1) - result.Node.Topology = "container_hostname" - result.Node.ID = id - result.Children = result.Children.Add(n) - result.Shape = Hexagon - result.Stack = true - return RenderableNodes{id: result} -} - -// MapCountContainers maps 1:1 container image nodes, counting -// the number of containers grouped together and putting -// that info in the minor label. -func MapCountContainers(n RenderableNode, _ report.Networks) RenderableNodes { - if n.Pseudo { - return RenderableNodes{n.ID: n} - } - - output := n.Copy() - containers, _ := n.Node.Counters.Lookup(ContainersKey) - if containers == 1 { - output.LabelMinor = "1 container" - } else { - output.LabelMinor = fmt.Sprintf("%d containers", containers) - } - return RenderableNodes{output.ID: output} -} - -// MapCountPods maps 1:1 service nodes, counting the number of pods grouped -// together and putting that info in the minor label. -func MapCountPods(n RenderableNode, _ report.Networks) RenderableNodes { - if n.Pseudo { - return RenderableNodes{n.ID: n} - } - - output := n.Copy() - pods, _ := n.Node.Counters.Lookup(podsKey) - if pods == 1 { - output.LabelMinor = "1 pod" - } else { - output.LabelMinor = fmt.Sprintf("%d pods", pods) - } - return RenderableNodes{output.ID: output} + node := NewDerivedNode(id, n) + node.Latest = node.Latest. + Set(docker.ContainerHostname, timestamp, id). + Delete(docker.ContainerName) // TODO(paulbellamy): total hack to render these by hostname instead. + node.Counters = node.Counters.Add(n.Topology, 1) + return report.Nodes{id: node} } diff --git a/render/mapping_test.go b/render/mapping_test.go index 88be57d8cd..93382cdbd2 100644 --- a/render/mapping_test.go +++ b/render/mapping_test.go @@ -6,96 +6,24 @@ import ( "testing" "github.com/weaveworks/scope/probe/docker" - "github.com/weaveworks/scope/probe/endpoint" - "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) -func nrn(nmd report.Node) render.RenderableNode { - return render.NewRenderableNode("").WithNode(nmd) -} - -func TestMapEndpointIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"", nrn(report.MakeNodeWith(map[string]string{endpoint.Addr: "1.2.3.4", endpoint.Procspied: "true"})), false}, - {"", nrn(report.MakeNodeWith(map[string]string{endpoint.Port: "1234", endpoint.Procspied: "true"})), false}, - {"", nrn(report.MakeNodeWith(map[string]string{endpoint.Addr: "1.2.3.4", endpoint.Port: "1234", endpoint.Procspied: "true"})), true}, - {"", nrn(report.MakeNodeWith(map[string]string{endpoint.Addr: "1.2.3.4", endpoint.Port: "40000", endpoint.Procspied: "true"})), true}, - {"", nrn(report.MakeNodeWith(map[string]string{report.HostNodeID: report.MakeHostNodeID("foo"), endpoint.Addr: "10.0.0.1", endpoint.Port: "20001", endpoint.Procspied: "true"})), true}, - } { - testMap(t, render.MapEndpointIdentity, input) - } -} - -func TestMapProcessIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"basic process", nrn(report.MakeNodeWith(map[string]string{process.PID: "201"})), true}, - } { - testMap(t, render.MapProcessIdentity, input) - } -} - func TestMapProcess2Container(t *testing.T) { for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), true}, - {"basic process", nrn(report.MakeNodeWith(map[string]string{process.PID: "201", docker.ContainerID: "a1b2c3"})), true}, - {"uncontained", nrn(report.MakeNodeWith(map[string]string{process.PID: "201", report.HostNodeID: report.MakeHostNodeID("foo")})), true}, + {"empty", report.MakeNode(), true}, + {"basic process", report.MakeNodeWith(map[string]string{process.PID: "201", docker.ContainerID: "a1b2c3"}), true}, + {"uncontained", report.MakeNodeWith(map[string]string{process.PID: "201", report.HostNodeID: report.MakeHostNodeID("foo")}), true}, } { testMap(t, render.MapProcess2Container, input) } } -func TestMapContainerIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"basic container", nrn(report.MakeNodeWith(map[string]string{docker.ContainerID: "a1b2c3"})), true}, - } { - testMap(t, render.MapContainerIdentity, input) - } -} - -func TestMapContainerImageIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"basic image", nrn(report.MakeNodeWith(map[string]string{docker.ImageID: "a1b2c3"})), true}, - } { - testMap(t, render.MapContainerImageIdentity, input) - } -} - -func TestMapHostIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), true}, // TODO it's questionable if this is actually correct - } { - testMap(t, render.MapHostIdentity, input) - } -} - -func TestMapPodIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"basic pod", nrn(report.MakeNodeWith(map[string]string{kubernetes.PodID: "ping/pong", kubernetes.PodName: "pong"})), true}, - } { - testMap(t, render.MapPodIdentity, input) - } -} - -func TestMapServiceIdentity(t *testing.T) { - for _, input := range []testcase{ - {"empty", nrn(report.MakeNode()), false}, - {"basic service", nrn(report.MakeNodeWith(map[string]string{kubernetes.ServiceID: "ping/pong", kubernetes.ServiceName: "pong"})), true}, - } { - testMap(t, render.MapServiceIdentity, input) - } -} - type testcase struct { name string - n render.RenderableNode + n report.Node ok bool } diff --git a/render/memoise.go b/render/memoise.go index 052c49328e..83371d50ca 100644 --- a/render/memoise.go +++ b/render/memoise.go @@ -24,13 +24,13 @@ func Memoise(r Renderer) Renderer { } } -// Render produces a set of RenderableNodes given a Report. +// Render produces a set of Nodes given a Report. // Ideally, it just retrieves it from the cache, otherwise it calls through to // `r` and stores the result. -func (m *memoise) Render(rpt report.Report) RenderableNodes { +func (m *memoise) Render(rpt report.Report) report.Nodes { key := fmt.Sprintf("%s-%s", rpt.ID, m.id) if result, err := renderCache.Get(key); err == nil { - return result.(RenderableNodes) + return result.(report.Nodes) } output := m.Renderer.Render(rpt) renderCache.Set(key, output) diff --git a/render/memoise_test.go b/render/memoise_test.go index 5b1ea04244..d5f230f26b 100644 --- a/render/memoise_test.go +++ b/render/memoise_test.go @@ -9,16 +9,16 @@ import ( "github.com/weaveworks/scope/test/reflect" ) -type renderFunc func(r report.Report) render.RenderableNodes +type renderFunc func(r report.Report) report.Nodes -func (f renderFunc) Render(r report.Report) render.RenderableNodes { return f(r) } -func (f renderFunc) Stats(r report.Report) render.Stats { return render.Stats{} } +func (f renderFunc) Render(r report.Report) report.Nodes { return f(r) } +func (f renderFunc) Stats(r report.Report) render.Stats { return render.Stats{} } func TestMemoise(t *testing.T) { calls := 0 - r := renderFunc(func(rpt report.Report) render.RenderableNodes { + r := renderFunc(func(rpt report.Report) report.Nodes { calls++ - return render.RenderableNodes{rpt.ID: render.NewRenderableNode(rpt.ID)} + return report.Nodes{rpt.ID: report.MakeNode().WithID(rpt.ID)} }) m := render.Memoise(r) rpt1 := report.MakeReport() diff --git a/render/render.go b/render/render.go index a57f99be50..8c045c8e2f 100644 --- a/render/render.go +++ b/render/render.go @@ -4,9 +4,9 @@ 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 Nodes. type Renderer interface { - Render(report.Report) RenderableNodes + Render(report.Report) report.Nodes Stats(report.Report) Stats } @@ -31,9 +31,9 @@ func MakeReduce(renderers ...Renderer) Renderer { return Memoise(&r) } -// Render produces a set of RenderableNodes given a Report. -func (r *Reduce) Render(rpt report.Report) RenderableNodes { - result := RenderableNodes{} +// Render produces a set of Nodes given a Report. +func (r *Reduce) Render(rpt report.Report) report.Nodes { + result := report.Nodes{} for _, renderer := range *r { result = result.Merge(renderer.Render(rpt)) } @@ -49,8 +49,8 @@ func (r *Reduce) Stats(rpt report.Report) Stats { return result } -// Map is a Renderer which produces a set of RenderableNodes from the set of -// RenderableNodes produced by another Renderer. +// Map is a Renderer which produces a set of Nodes from the set of +// Nodes produced by another Renderer. type Map struct { MapFunc Renderer @@ -61,25 +61,12 @@ func MakeMap(f MapFunc, r Renderer) Renderer { return Memoise(&Map{f, r}) } -// Render transforms a set of RenderableNodes produces by another Renderer. +// Render transforms a set of Nodes produces by another Renderer. // using a map function -func (m *Map) Render(rpt report.Report) RenderableNodes { - output, _ := m.render(rpt) - return output -} - -// Stats implements Renderer -func (m *Map) Stats(rpt report.Report) Stats { - // There doesn't seem to be an instance where we want stats to recurse - // through Maps - for instance we don't want to see the number of filtered - // processes in the container renderer. - return Stats{} -} - -func (m *Map) render(rpt report.Report) (RenderableNodes, map[string]report.IDList) { +func (m *Map) Render(rpt report.Report) report.Nodes { var ( input = m.Renderer.Render(rpt) - output = RenderableNodes{} + output = report.Nodes{} mapped = map[string]report.IDList{} // input node ID -> output node IDs adjacencies = map[string]report.IDList{} // output node ID -> input node Adjacencies localNetworks = LocalNetworks(rpt) @@ -112,5 +99,13 @@ func (m *Map) render(rpt report.Report) (RenderableNodes, map[string]report.IDLi output[outNodeID] = outNode } - return output, mapped + return output +} + +// Stats implements Renderer +func (m *Map) Stats(rpt report.Report) Stats { + // There doesn't seem to be an instance where we want stats to recurse + // through Maps - for instance we don't want to see the number of filtered + // processes in the container renderer. + return Stats{} } diff --git a/render/render_test.go b/render/render_test.go index 7ac6924f5a..b05973ecc5 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -10,21 +10,21 @@ import ( ) type mockRenderer struct { - render.RenderableNodes + report.Nodes } -func (m mockRenderer) Render(rpt report.Report) render.RenderableNodes { return m.RenderableNodes } -func (m mockRenderer) Stats(rpt report.Report) render.Stats { return render.Stats{} } +func (m mockRenderer) Render(rpt report.Report) report.Nodes { return m.Nodes } +func (m mockRenderer) Stats(rpt report.Report) render.Stats { return render.Stats{} } func TestReduceRender(t *testing.T) { renderer := render.Reduce([]render.Renderer{ - mockRenderer{RenderableNodes: render.RenderableNodes{"foo": render.NewRenderableNode("foo")}}, - mockRenderer{RenderableNodes: render.RenderableNodes{"bar": render.NewRenderableNode("bar")}}, + mockRenderer{Nodes: report.Nodes{"foo": report.MakeNode().WithID("foo")}}, + mockRenderer{Nodes: report.Nodes{"bar": report.MakeNode().WithID("bar")}}, }) - want := render.RenderableNodes{ - "foo": render.NewRenderableNode("foo"), - "bar": render.NewRenderableNode("bar"), + want := report.Nodes{ + "foo": report.MakeNode().WithID("foo"), + "bar": report.MakeNode().WithID("bar"), } have := renderer.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { @@ -35,14 +35,14 @@ func TestReduceRender(t *testing.T) { func TestMapRender1(t *testing.T) { // 1. Check when we return false, the node gets filtered out mapper := render.Map{ - MapFunc: func(nodes render.RenderableNode, _ report.Networks) render.RenderableNodes { - return render.RenderableNodes{} + MapFunc: func(nodes report.Node, _ report.Networks) report.Nodes { + return report.Nodes{} }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": render.NewRenderableNode("foo"), + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo"), }}, } - want := render.RenderableNodes{} + want := report.Nodes{} have := mapper.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { t.Errorf("want %+v, have %+v", want, have) @@ -52,18 +52,18 @@ func TestMapRender1(t *testing.T) { func TestMapRender2(t *testing.T) { // 2. Check we can remap two nodes into one mapper := render.Map{ - MapFunc: func(nodes render.RenderableNode, _ report.Networks) render.RenderableNodes { - return render.RenderableNodes{ - "bar": render.NewRenderableNode("bar"), + MapFunc: func(nodes report.Node, _ report.Networks) report.Nodes { + return report.Nodes{ + "bar": report.MakeNode().WithID("bar"), } }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": render.NewRenderableNode("foo"), - "baz": render.NewRenderableNode("baz"), + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo"), + "baz": report.MakeNode().WithID("baz"), }}, } - want := render.RenderableNodes{ - "bar": render.NewRenderableNode("bar"), + want := report.Nodes{ + "bar": report.MakeNode().WithID("bar"), } have := mapper.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { @@ -74,18 +74,18 @@ func TestMapRender2(t *testing.T) { func TestMapRender3(t *testing.T) { // 3. Check we can remap adjacencies mapper := render.Map{ - MapFunc: func(nodes render.RenderableNode, _ report.Networks) render.RenderableNodes { + MapFunc: func(nodes report.Node, _ report.Networks) report.Nodes { id := "_" + nodes.ID - return render.RenderableNodes{id: render.NewRenderableNode(id)} + return report.Nodes{id: report.MakeNode().WithID(id)} }, - Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{ - "foo": render.NewRenderableNode("foo").WithNode(report.MakeNode().WithAdjacent("baz")), - "baz": render.NewRenderableNode("baz").WithNode(report.MakeNode().WithAdjacent("foo")), + Renderer: mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode().WithID("foo").WithAdjacent("baz"), + "baz": report.MakeNode().WithID("baz").WithAdjacent("foo"), }}, } - want := render.RenderableNodes{ - "_foo": render.NewRenderableNode("_foo").WithNode(report.MakeNode().WithAdjacent("_baz")), - "_baz": render.NewRenderableNode("_baz").WithNode(report.MakeNode().WithAdjacent("_foo")), + want := report.Nodes{ + "_foo": report.MakeNode().WithID("_foo").WithAdjacent("_baz"), + "_baz": report.MakeNode().WithID("_baz").WithAdjacent("_foo"), } have := mapper.Render(report.MakeReport()) if !reflect.DeepEqual(want, have) { diff --git a/render/renderable_node.go b/render/renderable_node.go deleted file mode 100644 index 97af4e136b..0000000000 --- a/render/renderable_node.go +++ /dev/null @@ -1,241 +0,0 @@ -package render - -import ( - "strings" - - "github.com/weaveworks/scope/probe/docker" - "github.com/weaveworks/scope/probe/kubernetes" - "github.com/weaveworks/scope/report" -) - -// RenderableNode is the data type that's yielded to the JavaScript layer as -// 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 - Children RenderableNodeSet `json:"children,omitempty"` // Nodes which have been grouped into this one - ControlNode string `json:"control_node"` // ID of node from which to show the controls in the UI - Shape string `json:"shape"` // Shape node should be rendered as - Stack bool `json:"stack"` // Should UI render this node as a stack? - - report.EdgeMetadata `json:"metadata"` // Numeric sums - report.Node -} - -// Shapes that are allowed -const ( - Circle = "circle" - Square = "square" - Heptagon = "heptagon" - Hexagon = "hexagon" - Cloud = "cloud" -) - -// NewRenderableNode makes a new RenderableNode -func NewRenderableNode(id string) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: "", - LabelMinor: "", - Rank: "", - Pseudo: false, - EdgeMetadata: report.EdgeMetadata{}, - Node: report.MakeNode(), - Shape: Circle, - } -} - -// NewRenderableNodeWith makes a new RenderableNode with some fields filled in -func NewRenderableNodeWith(id, major, minor, rank string, node RenderableNode) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: major, - LabelMinor: minor, - Rank: rank, - Pseudo: false, - Children: node.Children.Copy(), - EdgeMetadata: node.EdgeMetadata.Copy(), - Node: node.Node.Copy(), - Shape: Circle, - } -} - -// NewDerivedNode create a renderable node based on node, but with a new ID -func NewDerivedNode(id string, node RenderableNode) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: "", - LabelMinor: "", - Rank: "", - Pseudo: node.Pseudo, - Children: node.Children.Copy(), - EdgeMetadata: node.EdgeMetadata.Copy(), - Node: node.Node.Copy(), - ControlNode: "", // Do not propagate ControlNode when making a derived node! - Shape: Circle, - } -} - -func newDerivedPseudoNode(id, major string, node RenderableNode) RenderableNode { - return RenderableNode{ - ID: id, - LabelMajor: major, - LabelMinor: "", - Rank: "", - Pseudo: true, - Children: node.Children.Copy(), - EdgeMetadata: node.EdgeMetadata.Copy(), - Node: node.Node.Copy(), - Shape: Circle, - } -} - -// WithNode creates a new RenderableNode based on rn, with n -func (rn RenderableNode) WithNode(n report.Node) RenderableNode { - result := rn.Copy() - result.Node = result.Node.Merge(n) - return result -} - -// WithParents creates a new RenderableNode based on rn, where n has the given parents set -func (rn RenderableNode) WithParents(p report.Sets) RenderableNode { - result := rn.Copy() - result.Node.Parents = p - return result -} - -// Merge merges rn with other and returns a new RenderableNode -func (rn RenderableNode) Merge(other RenderableNode) RenderableNode { - result := rn.Copy() - - if result.LabelMajor == "" { - result.LabelMajor = other.LabelMajor - } - - if result.LabelMinor == "" { - result.LabelMinor = other.LabelMinor - } - - if result.Rank == "" { - result.Rank = other.Rank - } - - if result.ControlNode == "" { - result.ControlNode = other.ControlNode - } - - if result.Pseudo != other.Pseudo { - panic(result.ID) - } - - result.Stack = result.Stack || rn.Stack - result.Children = rn.Children.Merge(other.Children) - result.EdgeMetadata = rn.EdgeMetadata.Merge(other.EdgeMetadata) - result.Node = rn.Node.Merge(other.Node) - - return result -} - -// Copy makes a deep copy of rn -func (rn RenderableNode) Copy() RenderableNode { - return RenderableNode{ - ID: rn.ID, - LabelMajor: rn.LabelMajor, - LabelMinor: rn.LabelMinor, - Rank: rn.Rank, - Pseudo: rn.Pseudo, - Children: rn.Children.Copy(), - EdgeMetadata: rn.EdgeMetadata.Copy(), - Node: rn.Node.Copy(), - ControlNode: rn.ControlNode, - Shape: rn.Shape, - Stack: rn.Stack, - } -} - -// IsStopped checks if the RenderableNode is a stopped docker container. -func (rn RenderableNode) IsStopped() bool { - containerState, ok := rn.Latest.Lookup(docker.ContainerState) - return !ok || containerState != docker.StateStopped -} - -// IsSystem checks if the RenderableNode is a system container/pod/etc. -func (rn RenderableNode) IsSystem() bool { - containerName, _ := rn.Latest.Lookup(docker.ContainerName) - if _, ok := systemContainerNames[containerName]; ok { - return false - } - imageName, _ := rn.Latest.Lookup(docker.ImageName) - imagePrefix := strings.SplitN(imageName, ":", 2)[0] // :( - if _, ok := systemImagePrefixes[imagePrefix]; ok { - return false - } - roleLabel, _ := rn.Latest.Lookup(docker.LabelPrefix + "works.weave.role") - if roleLabel == "system" { - return false - } - namespace, _ := rn.Latest.Lookup(kubernetes.Namespace) - if namespace == "kube-system" { - return false - } - podName, _ := rn.Latest.Lookup(docker.LabelPrefix + "io.kubernetes.pod.name") - if strings.HasPrefix(podName, "kube-system/") { - return false - } - return true -} - -// Prune returns a copy of the RenderableNode with all information not -// strictly necessary for rendering nodes and edges stripped away. -// Specifically, that means cutting out parts of the Node. -func (rn RenderableNode) Prune() RenderableNode { - cp := rn.Copy() - cp.Node = report.MakeNode().WithAdjacent(cp.Node.Adjacency...) - cp.Children = MakeRenderableNodeSet() - rn.Children.ForEach(func(n RenderableNode) { - cp.Children = cp.Children.Add(n.Prune()) - }) - return cp -} - -// RenderableNodes is a set of RenderableNodes -type RenderableNodes map[string]RenderableNode - -// Copy produces a deep copy of the RenderableNodes -func (rns RenderableNodes) Copy() RenderableNodes { - result := RenderableNodes{} - for key, value := range rns { - result[key] = value.Copy() - } - return result -} - -// Merge merges two sets of RenderableNodes, returning a new set. -func (rns RenderableNodes) Merge(other RenderableNodes) RenderableNodes { - result := RenderableNodes{} - for key, value := range rns { - result[key] = value - } - for key, value := range other { - existing, ok := result[key] - if ok { - value = value.Merge(existing) - } - result[key] = value - } - return result -} - -// Prune returns a copy of the RenderableNodes with all information not -// strictly necessary for rendering nodes and edges in the UI cut away. -func (rns RenderableNodes) Prune() RenderableNodes { - cp := rns.Copy() - for id, rn := range cp { - cp[id] = rn.Prune() - } - return cp -} diff --git a/render/renderable_node_set_test.go b/render/renderable_node_set_test.go deleted file mode 100644 index 9ffcdeb756..0000000000 --- a/render/renderable_node_set_test.go +++ /dev/null @@ -1,272 +0,0 @@ -package render_test - -import ( - "fmt" - "testing" - - "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/report" - "github.com/weaveworks/scope/test/reflect" -) - -var benchmarkResult render.RenderableNodeSet - -type nodeSpec struct { - topology string - id string -} - -func renderableNode(n report.Node) render.RenderableNode { - node := render.NewRenderableNode(n.ID) - node.Topology = n.Topology - return node -} - -func TestMakeRenderableNodeSet(t *testing.T) { - for _, testcase := range []struct { - inputs []nodeSpec - wants []nodeSpec - }{ - {inputs: nil, wants: nil}, - {inputs: []nodeSpec{}, wants: []nodeSpec{}}, - { - inputs: []nodeSpec{{"", "a"}}, - wants: []nodeSpec{{"", "a"}}, - }, - { - inputs: []nodeSpec{{"", "a"}, {"", "a"}, {"1", "a"}}, - wants: []nodeSpec{{"", "a"}, {"1", "a"}}, - }, - { - inputs: []nodeSpec{{"", "b"}, {"", "c"}, {"", "a"}}, - wants: []nodeSpec{{"", "a"}, {"", "b"}, {"", "c"}}, - }, - { - inputs: []nodeSpec{{"2", "a"}, {"3", "a"}, {"1", "a"}}, - wants: []nodeSpec{{"1", "a"}, {"2", "a"}, {"3", "a"}}, - }, - } { - var ( - inputs []render.RenderableNode - wants []render.RenderableNode - ) - for _, spec := range testcase.inputs { - node := render.NewRenderableNode(spec.id) - node.Topology = spec.topology - inputs = append(inputs, node) - } - for _, spec := range testcase.wants { - node := render.NewRenderableNode(spec.id) - node.Topology = spec.topology - wants = append(wants, node) - } - if want, have := render.MakeRenderableNodeSet(wants...), render.MakeRenderableNodeSet(inputs...); !reflect.DeepEqual(want, have) { - t.Errorf("%#v: want %#v, have %#v", inputs, wants, have) - } - } -} - -func BenchmarkMakeRenderableNodeSet(b *testing.B) { - nodes := []render.RenderableNode{} - for i := 1000; i >= 0; i-- { - node := report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ - "a": "1", - "b": "2", - }) - rn := render.NewRenderableNode(node.ID) - rn.Node = node - nodes = append(nodes, rn) - } - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - benchmarkResult = render.MakeRenderableNodeSet(nodes...) - } -} - -func TestRenderableNodeSetAdd(t *testing.T) { - for _, testcase := range []struct { - input render.RenderableNodeSet - nodes []render.RenderableNode - want render.RenderableNodeSet - }{ - { - input: render.RenderableNodeSet{}, - nodes: []render.RenderableNode{}, - want: render.RenderableNodeSet{}, - }, - { - input: render.EmptyRenderableNodeSet, - nodes: []render.RenderableNode{}, - want: render.EmptyRenderableNodeSet, - }, - { - input: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - nodes: []render.RenderableNode{}, - want: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - }, - { - input: render.EmptyRenderableNodeSet, - nodes: []render.RenderableNode{renderableNode(report.MakeNode().WithID("a"))}, - want: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - }, - { - input: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - nodes: []render.RenderableNode{renderableNode(report.MakeNode().WithID("a"))}, - want: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - }, - { - input: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("b"))), - nodes: []render.RenderableNode{ - renderableNode(report.MakeNode().WithID("a")), - renderableNode(report.MakeNode().WithID("b")), - }, - want: render.MakeRenderableNodeSet( - renderableNode(report.MakeNode().WithID("a")), - renderableNode(report.MakeNode().WithID("b")), - ), - }, - { - input: render.MakeRenderableNodeSet(renderableNode(report.MakeNode().WithID("a"))), - nodes: []render.RenderableNode{ - renderableNode(report.MakeNode().WithID("c")), - renderableNode(report.MakeNode().WithID("b")), - }, - want: render.MakeRenderableNodeSet( - renderableNode(report.MakeNode().WithID("a")), - renderableNode(report.MakeNode().WithID("b")), - renderableNode(report.MakeNode().WithID("c")), - ), - }, - { - input: render.MakeRenderableNodeSet( - renderableNode(report.MakeNode().WithID("a")), - renderableNode(report.MakeNode().WithID("c")), - ), - nodes: []render.RenderableNode{ - renderableNode(report.MakeNode().WithID("b")), - renderableNode(report.MakeNode().WithID("b")), - renderableNode(report.MakeNode().WithID("b")), - }, - want: render.MakeRenderableNodeSet( - renderableNode(report.MakeNode().WithID("a")), - renderableNode(report.MakeNode().WithID("b")), - renderableNode(report.MakeNode().WithID("c")), - ), - }, - } { - originalLen := testcase.input.Size() - if want, have := testcase.want, testcase.input.Add(testcase.nodes...); !reflect.DeepEqual(want, have) { - t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.nodes, want, have) - } - if testcase.input.Size() != originalLen { - t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.nodes) - } - } -} - -func BenchmarkRenderableNodeSetAdd(b *testing.B) { - n := render.EmptyRenderableNodeSet - for i := 0; i < 600; i++ { - n = n.Add( - renderableNode(report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ - "a": "1", - "b": "2", - })), - ) - } - - node := renderableNode(report.MakeNode().WithID("401.5").WithLatests(map[string]string{ - "a": "1", - "b": "2", - })) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - benchmarkResult = n.Add(node) - } -} - -func TestRenderableNodeSetMerge(t *testing.T) { - for _, testcase := range []struct { - input render.RenderableNodeSet - other render.RenderableNodeSet - want render.RenderableNodeSet - }{ - {input: render.RenderableNodeSet{}, other: render.RenderableNodeSet{}, want: render.RenderableNodeSet{}}, - {input: render.EmptyRenderableNodeSet, other: render.EmptyRenderableNodeSet, want: render.EmptyRenderableNodeSet}, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - other: render.EmptyRenderableNodeSet, - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - }, - { - input: render.EmptyRenderableNodeSet, - other: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - }, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - other: render.MakeRenderableNodeSet(render.NewRenderableNode("b")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("b")), - }, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("b")), - other: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("b")), - }, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - other: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - }, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("c")), - other: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("b")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("b"), render.NewRenderableNode("c")), - }, - { - input: render.MakeRenderableNodeSet(render.NewRenderableNode("b")), - other: render.MakeRenderableNodeSet(render.NewRenderableNode("a")), - want: render.MakeRenderableNodeSet(render.NewRenderableNode("a"), render.NewRenderableNode("b")), - }, - } { - originalLen := testcase.input.Size() - if want, have := testcase.want, testcase.input.Merge(testcase.other); !reflect.DeepEqual(want, have) { - t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.other, want, have) - } - if testcase.input.Size() != originalLen { - t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.other) - } - } -} - -func BenchmarkRenderableNodeSetMerge(b *testing.B) { - n, other := render.RenderableNodeSet{}, render.RenderableNodeSet{} - for i := 0; i < 600; i++ { - n = n.Add( - renderableNode(report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ - "a": "1", - "b": "2", - })), - ) - } - - for i := 400; i < 1000; i++ { - other = other.Add( - renderableNode(report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ - "c": "1", - "d": "2", - })), - ) - } - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - benchmarkResult = n.Merge(other) - } -} diff --git a/render/renderable_node_test.go b/render/renderable_node_test.go deleted file mode 100644 index e6bf6745c7..0000000000 --- a/render/renderable_node_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package render_test - -import ( - "reflect" - "testing" - - "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/report" - "github.com/weaveworks/scope/test" -) - -func TestMergeRenderableNodes(t *testing.T) { - nodes1 := render.RenderableNodes{ - "foo": render.NewRenderableNode("foo"), - "bar": render.NewRenderableNode("bar"), - } - nodes2 := render.RenderableNodes{ - "bar": render.NewRenderableNode("bar"), - "baz": render.NewRenderableNode("baz"), - } - want := (render.RenderableNodes{ - "foo": render.NewRenderableNode("foo"), - "bar": render.NewRenderableNode("bar"), - "baz": render.NewRenderableNode("baz"), - }).Prune() - have := nodes1.Merge(nodes2).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) - } -} - -func TestMergeRenderableNode(t *testing.T) { - node1 := render.RenderableNode{ - ID: "foo", - LabelMajor: "", - LabelMinor: "minor", - Rank: "", - Pseudo: false, - Node: report.MakeNode().WithAdjacent("a1"), - Children: render.MakeRenderableNodeSet(render.NewRenderableNode("child1")), - } - node2 := render.RenderableNode{ - ID: "foo", - LabelMajor: "major", - LabelMinor: "", - Rank: "rank", - Pseudo: false, - Node: report.MakeNode().WithAdjacent("a2"), - Children: render.MakeRenderableNodeSet(render.NewRenderableNode("child2")), - } - want := render.RenderableNode{ - ID: "foo", - LabelMajor: "major", - LabelMinor: "minor", - Rank: "rank", - Pseudo: false, - Node: report.MakeNode().WithID("foo").WithAdjacent("a1").WithAdjacent("a2"), - Children: render.MakeRenderableNodeSet(render.NewRenderableNode("child1"), render.NewRenderableNode("child2")), - EdgeMetadata: report.EdgeMetadata{}, - }.Prune() - have := node1.Merge(node2).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) - } -} diff --git a/render/selectors.go b/render/selectors.go index a141e126c1..683382872a 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -6,11 +6,12 @@ import ( // TopologySelector selects a single topology from a report. // NB it is also a Renderer! -type TopologySelector func(r report.Report) RenderableNodes +type TopologySelector string // Render implements Renderer -func (t TopologySelector) Render(r report.Report) RenderableNodes { - return t(r) +func (t TopologySelector) Render(r report.Report) report.Nodes { + topology, _ := r.Topology(string(t)) + return topology.Nodes } // Stats implements Renderer @@ -18,60 +19,14 @@ func (t TopologySelector) Stats(r report.Report) Stats { return Stats{} } -// MakeRenderableNodes converts a topology to a set of RenderableNodes -func MakeRenderableNodes(t report.Topology) RenderableNodes { - result := RenderableNodes{} - for id, nmd := range t.Nodes { - result[id] = NewRenderableNode(id).WithNode(nmd) - } - - // Push EdgeMetadata to both ends of the edges - for srcID, srcNode := range result { - srcNode.Edges.ForEach(func(dstID string, emd report.EdgeMetadata) { - srcNode.EdgeMetadata = srcNode.EdgeMetadata.Flatten(emd) - - dstNode := result[dstID] - dstNode.EdgeMetadata = dstNode.EdgeMetadata.Flatten(emd.Reversed()) - result[dstID] = dstNode - }) - result[srcID] = srcNode - } - return result -} - +// The topology selectors implement a Renderer which fetch the nodes from the +// various report topologies. var ( - // SelectEndpoint selects the endpoint topology. - SelectEndpoint = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Endpoint) - }) - - // SelectProcess selects the process topology. - SelectProcess = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Process) - }) - - // SelectContainer selects the container topology. - SelectContainer = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Container) - }) - - // SelectContainerImage selects the container image topology. - SelectContainerImage = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.ContainerImage) - }) - - // SelectHost selects the address topology. - SelectHost = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Host) - }) - - // SelectPod selects the pod topology. - SelectPod = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Pod) - }) - - // SelectService selects the service topology. - SelectService = TopologySelector(func(r report.Report) RenderableNodes { - return MakeRenderableNodes(r.Service) - }) + SelectEndpoint = TopologySelector(report.Endpoint) + SelectProcess = TopologySelector(report.Process) + SelectContainer = TopologySelector(report.Container) + SelectContainerImage = TopologySelector(report.ContainerImage) + SelectHost = TopologySelector(report.Host) + SelectPod = TopologySelector(report.Pod) + SelectService = TopologySelector(report.Service) ) diff --git a/render/selectors_test.go b/render/selectors_test.go deleted file mode 100644 index ee01e983b5..0000000000 --- a/render/selectors_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/report" - "github.com/weaveworks/scope/test" - "github.com/weaveworks/scope/test/reflect" -) - -func TestMakeRenderableNodes(t *testing.T) { - - var ( - newu64 = func(value uint64) *uint64 { return &value } - srcNodeID = "srcNode" - dstNode1ID = "dstNode1" - dstNode2ID = "dstNode2" - srcNode = report.MakeNode(). - WithEdge(dstNode1ID, report.EdgeMetadata{EgressPacketCount: newu64(100), EgressByteCount: newu64(1000)}). - WithEdge(dstNode2ID, report.EdgeMetadata{EgressPacketCount: newu64(200), EgressByteCount: newu64(2000)}) - dstNode1 = report.MakeNode() - dstNode2 = report.MakeNode() - topology = report.MakeTopology(). - AddNode(srcNodeID, srcNode). - AddNode(dstNode1ID, dstNode1). - AddNode(dstNode2ID, dstNode2) - ) - - result := render.MakeRenderableNodes(topology) - mustLookup := func(id string) render.RenderableNode { - node, ok := result[id] - if !ok { - t.Fatalf("Expected result to contain node: %q, got: %v", id, result) - } - return node - } - - // Source nodes should have the flattened edge metadata - { - have := mustLookup(srcNodeID).EdgeMetadata - want := report.EdgeMetadata{EgressPacketCount: newu64(300), EgressByteCount: newu64(3000)} - if !reflect.DeepEqual(want, have) { - t.Errorf(test.Diff(want, have)) - } - } - - // Result destination nodes should have the reverse of the source nodes - { - have := mustLookup(dstNode1ID).EdgeMetadata - want := report.EdgeMetadata{IngressPacketCount: newu64(100), IngressByteCount: newu64(1000)} - if !reflect.DeepEqual(want, have) { - t.Errorf(test.Diff(want, have)) - } - - have = mustLookup(dstNode2ID).EdgeMetadata - want = report.EdgeMetadata{IngressPacketCount: newu64(200), IngressByteCount: newu64(2000)} - if !reflect.DeepEqual(want, have) { - t.Errorf(test.Diff(want, have)) - } - } -} diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go index 71c4c6dc9d..47b42a6e8e 100644 --- a/render/short_lived_connections_test.go +++ b/render/short_lived_connections_test.go @@ -2,7 +2,6 @@ package render_test import ( "fmt" - "reflect" "testing" "github.com/weaveworks/scope/probe/docker" @@ -10,7 +9,6 @@ import ( "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" - "github.com/weaveworks/scope/test" ) var ( @@ -68,32 +66,18 @@ var ( }, }, } - - want = (render.RenderableNodes{ - render.IncomingInternetID: { - ID: render.IncomingInternetID, - LabelMajor: render.InboundMajor, - LabelMinor: render.InboundMinor, - Pseudo: true, - Shape: "cloud", - Node: report.MakeNode().WithAdjacent(render.MakeContainerID(containerID)), - }, - render.MakeContainerID(containerID): { - ID: render.MakeContainerID(containerID), - LabelMajor: containerName, - LabelMinor: serverHostID, - Rank: "", - Pseudo: false, - Shape: "hexagon", - Node: report.MakeNode(), - ControlNode: containerNodeID, - }, - }).Prune() ) func TestShortLivedInternetNodeConnections(t *testing.T) { - have := (render.ContainerWithImageNameRenderer.Render(rpt)).Prune() - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) + have := render.ContainerWithImageNameRenderer.Render(rpt).Prune() + + // Conntracked-only connections from the internet should be assigned to the internet pseudonode + internet, ok := have[render.IncomingInternetID] + if !ok { + t.Fatal("Expected output to have an incoming internet node") + } + + if !internet.Adjacency.Contains(report.MakeContainerNodeID(containerID)) { + t.Errorf("Expected internet node to have adjacency to %s, but only had %v", report.MakeContainerNodeID(containerID), internet.Adjacency) } } diff --git a/render/topologies.go b/render/topologies.go index 0603a1dc52..b7656bda41 100644 --- a/render/topologies.go +++ b/render/topologies.go @@ -1,20 +1,15 @@ package render import ( - "fmt" "net" "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" - "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" ) // EndpointRenderer is a Renderer which produces a renderable endpoint graph. -var EndpointRenderer = MakeMap( - MapEndpointIdentity, - SelectEndpoint, -) +var EndpointRenderer = FilterNonProcspied(SelectEndpoint) // ProcessRenderer is a Renderer which produces a renderable process // graph by merging the endpoint graph and the process topology. @@ -23,10 +18,7 @@ var ProcessRenderer = MakeReduce( MapEndpoint2Process, EndpointRenderer, ), - MakeMap( - MapProcessIdentity, - SelectProcess, - ), + SelectProcess, ) // processWithContainerNameRenderer is a Renderer which produces a process @@ -35,30 +27,26 @@ type processWithContainerNameRenderer struct { Renderer } -func (r processWithContainerNameRenderer) Render(rpt report.Report) RenderableNodes { +func (r processWithContainerNameRenderer) Render(rpt report.Report) report.Nodes { processes := r.Renderer.Render(rpt) - containers := MakeMap( - MapContainerIdentity, - SelectContainer, - ).Render(rpt) + containers := SelectContainer.Render(rpt) - outputs := RenderableNodes{} + outputs := report.Nodes{} for id, p := range processes { outputs[id] = p - pid, ok := p.Node.Latest.Lookup(process.PID) + containerID, timestamp, ok := p.Latest.LookupEntry(docker.ContainerID) if !ok { continue } - containerID, ok := p.Node.Latest.Lookup(docker.ContainerID) - if !ok { - continue - } - container, ok := containers[MakeContainerID(containerID)] + container, ok := containers[report.MakeContainerNodeID(containerID)] if !ok { continue } output := p.Copy() - output.LabelMinor = fmt.Sprintf("%s (%s:%s)", report.ExtractHostID(p.Node), container.LabelMajor, pid) + output.Latest = output.Latest.Set(docker.ContainerID, timestamp, containerID) + if containerName, timestamp, ok := container.Latest.LookupEntry(docker.ContainerName); ok { + output.Latest = output.Latest.Set(docker.ContainerName, timestamp, containerName) + } outputs[id] = output } return outputs @@ -71,11 +59,8 @@ var ProcessWithContainerNameRenderer = processWithContainerNameRenderer{ProcessR // ProcessNameRenderer is a Renderer which produces a renderable process // name graph by munging the progess graph. var ProcessNameRenderer = MakeMap( - MapCountProcessName, - MakeMap( - MapProcess2Name, - ProcessRenderer, - ), + MapProcess2Name, + ProcessRenderer, ) // ContainerRenderer is a Renderer which produces a renderable container @@ -85,9 +70,9 @@ var ProcessNameRenderer = MakeMap( // including the ProcessRenderer once. var ContainerRenderer = MakeReduce( MakeFilter( - func(n RenderableNode) bool { - _, inContainer := n.Node.Latest.Lookup(docker.ContainerID) - _, isConnected := n.Node.Latest.Lookup(IsConnected) + func(n report.Node) bool { + _, inContainer := n.Latest.Lookup(docker.ContainerID) + _, isConnected := n.Latest.Lookup(IsConnected) return inContainer || isConnected }, MakeMap( @@ -114,10 +99,7 @@ var ContainerRenderer = MakeReduce( ), )), - MakeMap( - MapContainerIdentity, - SelectContainer, - ), + SelectContainer, ) type containerWithHostIPsRenderer struct { @@ -126,22 +108,19 @@ type containerWithHostIPsRenderer struct { // Render produces a process graph where the ips for host network mode are set // to the host's IPs. -func (r containerWithHostIPsRenderer) Render(rpt report.Report) RenderableNodes { +func (r containerWithHostIPsRenderer) Render(rpt report.Report) report.Nodes { containers := r.Renderer.Render(rpt) - hosts := MakeMap( - MapHostIdentity, - SelectHost, - ).Render(rpt) + hosts := SelectHost.Render(rpt) - outputs := RenderableNodes{} + outputs := report.Nodes{} for id, c := range containers { outputs[id] = c - networkMode, ok := c.Node.Latest.Lookup(docker.ContainerNetworkMode) + networkMode, ok := c.Latest.Lookup(docker.ContainerNetworkMode) if !ok || networkMode != docker.NetworkModeHost { continue } - h, ok := hosts[MakeHostID(report.ExtractHostID(c.Node))] + h, ok := hosts[report.MakeHostNodeID(report.ExtractHostID(c))] if !ok { continue } @@ -172,26 +151,22 @@ type containerWithImageNameRenderer struct { // Render produces a process graph where the minor labels contain the // container name, if found. It also merges the image node metadata into the // container metadata. -func (r containerWithImageNameRenderer) Render(rpt report.Report) RenderableNodes { +func (r containerWithImageNameRenderer) Render(rpt report.Report) report.Nodes { containers := r.Renderer.Render(rpt) - images := MakeMap( - MapContainerImageIdentity, - SelectContainerImage, - ).Render(rpt) + images := SelectContainerImage.Render(rpt) - outputs := RenderableNodes{} + outputs := report.Nodes{} for id, c := range containers { outputs[id] = c - imageID, ok := c.Node.Latest.Lookup(docker.ImageID) + imageID, ok := c.Latest.Lookup(docker.ImageID) if !ok { continue } - image, ok := images[MakeContainerImageID(imageID)] + image, ok := images[report.MakeContainerImageNodeID(imageID)] if !ok { continue } output := c.Copy() - output.Rank = ImageNameWithoutVersion(image.LabelMajor) output.Latest = image.Latest.Merge(c.Latest) outputs[id] = output } @@ -204,31 +179,19 @@ var ContainerWithImageNameRenderer = containerWithImageNameRenderer{ContainerWit // ContainerImageRenderer is a Renderer which produces a renderable container // image graph by merging the container graph and the container image topology. -var ContainerImageRenderer = MakeMap( - MapCountContainers, +var ContainerImageRenderer = MakeReduce( MakeMap( - MapContainerImage2Name, - MakeReduce( - MakeMap( - MapContainer2ContainerImage, - ContainerRenderer, - ), - MakeMap( - MapContainerImageIdentity, - SelectContainerImage, - ), - ), + MapContainer2ContainerImage, + ContainerRenderer, ), + SelectContainerImage, ) // ContainerHostnameRenderer is a Renderer which produces a renderable container // by hostname graph.. var ContainerHostnameRenderer = MakeMap( - MapCountContainers, - MakeMap( - MapContainer2Hostname, - ContainerRenderer, - ), + MapContainer2Hostname, + ContainerRenderer, ) // HostRenderer is a Renderer which produces a renderable host @@ -253,45 +216,27 @@ var HostRenderer = MakeReduce( // Pods don't have a host id - #1142 // MakeMap( // MapX2Host, - // MakeMap( - // MapPodIdentity, // SelectPod, - // ), // ), - MakeMap( - MapHostIdentity, - SelectHost, - ), + SelectHost, ) // PodRenderer is a Renderer which produces a renderable kubernetes // graph by merging the container graph and the pods topology. -var PodRenderer = MakeMap( - MapCountContainers, - MakeReduce( - MakeMap( - MapContainer2Pod, - ContainerRenderer, - ), - MakeMap( - MapPodIdentity, - SelectPod, - ), +var PodRenderer = MakeReduce( + MakeMap( + MapContainer2Pod, + ContainerRenderer, ), + SelectPod, ) // PodServiceRenderer is a Renderer which produces a renderable kubernetes services // graph by merging the pods graph and the services topology. -var PodServiceRenderer = MakeMap( - MapCountPods, - MakeReduce( - MakeMap( - MapPod2Service, - PodRenderer, - ), - MakeMap( - MapServiceIdentity, - SelectService, - ), +var PodServiceRenderer = MakeReduce( + MakeMap( + MapPod2Service, + PodRenderer, ), + SelectService, ) diff --git a/render/topologies_test.go b/render/topologies_test.go index 379b4c1acd..be51fb4ab3 100644 --- a/render/topologies_test.go +++ b/render/topologies_test.go @@ -15,7 +15,7 @@ import ( func TestEndpointRenderer(t *testing.T) { have := render.EndpointRenderer.Render(fixture.Report).Prune() - want := expected.RenderedEndpoints + want := expected.RenderedEndpoints.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -23,7 +23,7 @@ func TestEndpointRenderer(t *testing.T) { func TestProcessRenderer(t *testing.T) { have := render.ProcessRenderer.Render(fixture.Report).Prune() - want := expected.RenderedProcesses + want := expected.RenderedProcesses.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -31,15 +31,15 @@ func TestProcessRenderer(t *testing.T) { func TestProcessNameRenderer(t *testing.T) { have := render.ProcessNameRenderer.Render(fixture.Report).Prune() - want := expected.RenderedProcessNames + want := expected.RenderedProcessNames.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } } func TestContainerRenderer(t *testing.T) { - have := (render.ContainerRenderer.Render(fixture.Report)).Prune() - want := expected.RenderedContainers + have := render.ContainerRenderer.Render(fixture.Report).Prune() + want := expected.RenderedContainers.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -53,8 +53,8 @@ func TestContainerFilterRenderer(t *testing.T) { docker.LabelPrefix + "works.weave.role": "system", }) have := render.FilterSystem(render.ContainerRenderer).Render(input).Prune() - want := expected.RenderedContainers.Copy() - delete(want, expected.ClientContainerID) + want := expected.RenderedContainers.Copy().Prune() + delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -68,11 +68,14 @@ func TestContainerWithHostIPsRenderer(t *testing.T) { nodes := render.ContainerWithHostIPsRenderer.Render(input) // Test host network nodes get the host IPs added. - haveNode, ok := nodes[render.MakeContainerID(fixture.ClientContainerID)] + haveNode, ok := nodes[fixture.ClientContainerNodeID] if !ok { t.Fatal("Expected output to have the client container node") } - have, _ := haveNode.Sets.Lookup(docker.ContainerIPs) + have, ok := haveNode.Sets.Lookup(docker.ContainerIPs) + if !ok { + t.Fatal("Container had no IPs set.") + } want := report.MakeStringSet("10.10.10.0") if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -81,7 +84,7 @@ func TestContainerWithHostIPsRenderer(t *testing.T) { func TestContainerImageRenderer(t *testing.T) { have := render.ContainerImageRenderer.Render(fixture.Report).Prune() - want := expected.RenderedContainerImages + want := expected.RenderedContainerImages.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -89,7 +92,7 @@ func TestContainerImageRenderer(t *testing.T) { func TestHostRenderer(t *testing.T) { have := render.HostRenderer.Render(fixture.Report).Prune() - want := expected.RenderedHosts + want := expected.RenderedHosts.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -97,7 +100,7 @@ func TestHostRenderer(t *testing.T) { func TestPodRenderer(t *testing.T) { have := render.PodRenderer.Render(fixture.Report).Prune() - want := expected.RenderedPods + want := expected.RenderedPods.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -116,9 +119,9 @@ func TestPodFilterRenderer(t *testing.T) { docker.LabelPrefix + "io.kubernetes.pod.name": "kube-system/foo", }) have := render.FilterSystem(render.PodRenderer).Render(input).Prune() - want := expected.RenderedPods.Copy() - delete(want, expected.ClientPodRenderedID) - delete(want, expected.ClientContainerID) + want := expected.RenderedPods.Copy().Prune() + delete(want, fixture.ClientPodNodeID) + delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -126,7 +129,7 @@ func TestPodFilterRenderer(t *testing.T) { func TestPodServiceRenderer(t *testing.T) { have := render.PodServiceRenderer.Render(fixture.Report).Prune() - want := expected.RenderedPodServices + want := expected.RenderedPodServices.Prune() if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/topology_diff_test.go b/render/topology_diff_test.go deleted file mode 100644 index bbc5234722..0000000000 --- a/render/topology_diff_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package render_test - -import ( - "reflect" - "sort" - "testing" - - "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/report" - "github.com/weaveworks/scope/test" -) - -// ByID is a sort interface for a RenderableNode slice. -type ByID []render.RenderableNode - -func (r ByID) Len() int { return len(r) } -func (r ByID) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r ByID) Less(i, j int) bool { return r[i].ID < r[j].ID } - -func TestTopoDiff(t *testing.T) { - nodea := render.RenderableNode{ - ID: "nodea", - LabelMajor: "Node A", - LabelMinor: "'ts an a", - Pseudo: false, - Node: report.MakeNode().WithAdjacent("nodeb"), - } - nodeap := nodea - nodeap.Adjacency = []string{ - "nodeb", - "nodeq", // not the same anymore - } - nodeb := render.RenderableNode{ - ID: "nodeb", - LabelMajor: "Node B", - } - - // Helper to make RenderableNode maps. - nodes := func(ns ...render.RenderableNode) render.RenderableNodes { - r := render.RenderableNodes{} - for _, n := range ns { - r[n.ID] = n - } - return r - } - - for _, c := range []struct { - label string - have, want render.Diff - }{ - { - label: "basecase: empty -> something", - have: render.TopoDiff(nodes(), nodes(nodea, nodeb)), - want: render.Diff{ - Add: []render.RenderableNode{nodea, nodeb}, - }, - }, - { - label: "basecase: something -> empty", - have: render.TopoDiff(nodes(nodea, nodeb), nodes()), - want: render.Diff{ - Remove: []string{"nodea", "nodeb"}, - }, - }, - { - label: "add and remove", - have: render.TopoDiff(nodes(nodea), nodes(nodeb)), - want: render.Diff{ - Add: []render.RenderableNode{nodeb}, - Remove: []string{"nodea"}, - }, - }, - { - label: "no change", - have: render.TopoDiff(nodes(nodea), nodes(nodea)), - want: render.Diff{}, - }, - { - label: "change a single node", - have: render.TopoDiff(nodes(nodea), nodes(nodeap)), - want: render.Diff{ - Update: []render.RenderableNode{nodeap}, - }, - }, - } { - sort.Strings(c.have.Remove) - sort.Sort(ByID(c.have.Add)) - sort.Sort(ByID(c.have.Update)) - if !reflect.DeepEqual(c.want, c.have) { - t.Errorf("%s - %s", c.label, test.Diff(c.want, c.have)) - } - } -} diff --git a/report/counters.go b/report/counters.go index 28ce8e4795..f7c5b7f08a 100644 --- a/report/counters.go +++ b/report/counters.go @@ -88,21 +88,28 @@ func (c Counters) Merge(other Counters) Counters { return Counters{output} } -func (c Counters) String() string { - if c.psMap == nil { - return "{}" - } - keys := []string{} - for _, k := range c.psMap.Keys() { - keys = append(keys, k) +// ForEach calls f for each k/v pair of counters. Keys are iterated in +// lexicographical order. +func (c Counters) ForEach(f func(key string, val int)) { + if c.psMap != nil { + keys := c.psMap.Keys() + sort.Strings(keys) + for _, key := range keys { + if val, ok := c.psMap.Lookup(key); ok { + f(key, val.(int)) + } + } } - sort.Strings(keys) +} +// String serializes Counters into a string. +func (c Counters) String() string { buf := bytes.NewBufferString("{") - for _, key := range keys { - val, _ := c.psMap.Lookup(key) - fmt.Fprintf(buf, "%s: %d, ", key, val) - } + prefix := "" + c.ForEach(func(k string, v int) { + fmt.Fprintf(buf, "%s%s: %d", prefix, k, v) + prefix = ", " + }) fmt.Fprintf(buf, "}") return buf.String() } diff --git a/report/counters_internal_test.go b/report/counters_internal_test.go index 7e8f29e5d4..0f912aebbe 100644 --- a/report/counters_internal_test.go +++ b/report/counters_internal_test.go @@ -132,3 +132,33 @@ func TestCountersEncoding(t *testing.T) { } } + +func TestCountersString(t *testing.T) { + { + var c Counters + have := c.String() + want := `{}` + if want != have { + t.Errorf("Expected: %s, Got %s", want, have) + } + } + + { + have := EmptyCounters.String() + want := `{}` + if want != have { + t.Errorf("Expected: %s, Got %s", want, have) + } + } + + { + have := EmptyCounters. + Add("foo", 1). + Add("bar", 2).String() + + want := `{bar: 2, foo: 1}` + if want != have { + t.Errorf("Expected: %s, Got %s", want, have) + } + } +} diff --git a/report/latest_map.go b/report/latest_map.go index 0b407fa971..117c2d7d22 100644 --- a/report/latest_map.go +++ b/report/latest_map.go @@ -86,14 +86,21 @@ func (m LatestMap) Merge(other LatestMap) LatestMap { // Lookup the value for the given key. func (m LatestMap) Lookup(key string) (string, bool) { + v, _, ok := m.LookupEntry(key) + return v, ok +} + +// LookupEntry returns the raw entry for the given key. +func (m LatestMap) LookupEntry(key string) (string, time.Time, bool) { if m.Map == nil { - return "", false + return "", time.Time{}, false } value, ok := m.Map.Lookup(key) if !ok { - return "", false + return "", time.Time{}, false } - return value.(LatestEntry).Value, true + e := value.(LatestEntry) + return e.Value, e.Timestamp, true } // Set the value for the given key. diff --git a/report/latest_map_internal_test.go b/report/latest_map_internal_test.go index e515374387..e0c15a50e2 100644 --- a/report/latest_map_internal_test.go +++ b/report/latest_map_internal_test.go @@ -29,6 +29,18 @@ func TestLatestMapAdd(t *testing.T) { }) } +func TestLatestMapLookupEntry(t *testing.T) { + now := time.Now() + entry := LatestEntry{Timestamp: now, Value: "Bar"} + have := EmptyLatestMap.Set("foo", entry.Timestamp, entry.Value) + if got, timestamp, ok := have.LookupEntry("foo"); !ok || got != entry.Value || !timestamp.Equal(entry.Timestamp) { + t.Errorf("got: %#v %v != expected %#v", got, timestamp, entry) + } + if got, timestamp, ok := have.LookupEntry("not found"); ok { + t.Errorf("found unexpected entry for %q: %#v %v", "not found", got, timestamp) + } +} + func TestLatestMapAddNil(t *testing.T) { now := time.Now() have := LatestMap{}.Set("foo", now, "Bar") diff --git a/report/metrics.go b/report/metrics.go index 2128ecaaeb..c911b41c8e 100644 --- a/report/metrics.go +++ b/report/metrics.go @@ -216,8 +216,8 @@ func (m Metric) LastSample() *Sample { // WireMetrics is the on-the-wire representation of Metrics. type WireMetrics struct { - Samples []Sample `json:"samples"` // On the wire, samples are sorted oldest to newest, - Min float64 `json:"min"` // the opposite order to how we store them internally. + Samples []Sample `json:"samples,omitempty"` // On the wire, samples are sorted oldest to newest, + Min float64 `json:"min"` // the opposite order to how we store them internally. Max float64 `json:"max"` First string `json:"first,omitempty"` Last string `json:"last,omitempty"` diff --git a/report/node.go b/report/node.go index 043811b363..bf731f63f8 100644 --- a/report/node.go +++ b/report/node.go @@ -20,6 +20,7 @@ type Node struct { Latest LatestMap `json:"latest,omitempty"` Metrics Metrics `json:"metrics,omitempty"` Parents Sets `json:"parents,omitempty"` + Children NodeSet `json:"children,omitempty"` } // MakeNode creates a new Node with no initial metadata. @@ -152,6 +153,27 @@ func (n Node) WithParents(parents Sets) Node { return result } +// PruneParents returns a fresh copy of n, without any parents. +func (n Node) PruneParents() Node { + result := n.Copy() + result.Parents = EmptySets + return result +} + +// WithChildren returns a fresh copy of n, with children merged in. +func (n Node) WithChildren(children NodeSet) Node { + result := n.Copy() + result.Children = result.Children.Merge(children) + return result +} + +// WithChild returns a fresh copy of n, with one child merged in. +func (n Node) WithChild(child Node) Node { + result := n.Copy() + result.Children = result.Children.Merge(MakeNodeSet(child)) + return result +} + // Copy returns a value copy of the Node. func (n Node) Copy() Node { cp := MakeNode() @@ -165,6 +187,7 @@ func (n Node) Copy() Node { cp.Latest = n.Latest.Copy() cp.Metrics = n.Metrics.Copy() cp.Parents = n.Parents.Copy() + cp.Children = n.Children.Copy() return cp } @@ -177,6 +200,8 @@ func (n Node) Merge(other Node) Node { } if cp.Topology == "" { cp.Topology = other.Topology + } else if other.Topology != "" && cp.Topology != other.Topology { + panic("Cannot merge nodes with different topology types: " + cp.Topology + " != " + other.Topology) } cp.Counters = cp.Counters.Merge(other.Counters) cp.Sets = cp.Sets.Merge(other.Sets) @@ -186,5 +211,21 @@ func (n Node) Merge(other Node) Node { cp.Latest = cp.Latest.Merge(other.Latest) cp.Metrics = cp.Metrics.Merge(other.Metrics) cp.Parents = cp.Parents.Merge(other.Parents) + cp.Children = cp.Children.Merge(other.Children) return cp } + +// Prune returns a copy of the Node with all information not strictly necessary +// for rendering nodes and edges stripped away. Specifically, that means +// cutting out parts of the Node. +func (n Node) Prune() Node { + prunedChildren := MakeNodeSet() + n.Children.ForEach(func(child Node) { + prunedChildren = prunedChildren.Add(child.Prune()) + }) + return MakeNode(). + WithID(n.ID). + WithTopology(n.Topology). + WithAdjacent(n.Adjacency.Copy()...). + WithChildren(prunedChildren) +} diff --git a/render/renderable_node_set.go b/report/node_set.go similarity index 54% rename from render/renderable_node_set.go rename to report/node_set.go index 7cf012610a..4b278d1bf1 100644 --- a/render/renderable_node_set.go +++ b/report/node_set.go @@ -1,4 +1,4 @@ -package render +package report import ( "bytes" @@ -13,35 +13,35 @@ import ( "github.com/weaveworks/scope/test/reflect" ) -// RenderableNodeSet is a set of nodes keyed on (Topology, ID). Clients must use +// NodeSet is a set of nodes keyed on ID. Clients must use // the Add method to add nodes -type RenderableNodeSet struct { +type NodeSet struct { psMap ps.Map } -// EmptyRenderableNodeSet is the empty set of nodes. -var EmptyRenderableNodeSet = RenderableNodeSet{ps.NewMap()} +// EmptyNodeSet is the empty set of nodes. +var EmptyNodeSet = NodeSet{ps.NewMap()} -// MakeRenderableNodeSet makes a new RenderableNodeSet with the given nodes. -func MakeRenderableNodeSet(nodes ...RenderableNode) RenderableNodeSet { - return EmptyRenderableNodeSet.Add(nodes...) +// MakeNodeSet makes a new NodeSet with the given nodes. +func MakeNodeSet(nodes ...Node) NodeSet { + return EmptyNodeSet.Add(nodes...) } -// Add adds the nodes to the RenderableNodeSet. Add is the only valid way to grow a -// RenderableNodeSet. Add returns the RenderableNodeSet to enable chaining. -func (n RenderableNodeSet) Add(nodes ...RenderableNode) RenderableNodeSet { +// Add adds the nodes to the NodeSet. Add is the only valid way to grow a +// NodeSet. Add returns the NodeSet to enable chaining. +func (n NodeSet) Add(nodes ...Node) NodeSet { result := n.psMap if result == nil { result = ps.NewMap() } for _, node := range nodes { - result = result.Set(fmt.Sprintf("%s|%s", node.Topology, node.ID), node) + result = result.Set(node.ID, node) } - return RenderableNodeSet{result} + return NodeSet{result} } -// Merge combines the two RenderableNodeSets and returns a new result. -func (n RenderableNodeSet) Merge(other RenderableNodeSet) RenderableNodeSet { +// Merge combines the two NodeSets and returns a new result. +func (n NodeSet) Merge(other NodeSet) NodeSet { nSize, otherSize := n.Size(), other.Size() if nSize == 0 { return other @@ -56,22 +56,22 @@ func (n RenderableNodeSet) Merge(other RenderableNodeSet) RenderableNodeSet { iter.ForEach(func(key string, otherVal interface{}) { result = result.Set(key, otherVal) }) - return RenderableNodeSet{result} + return NodeSet{result} } // Lookup the node 'key' -func (n RenderableNodeSet) Lookup(key string) (RenderableNode, bool) { +func (n NodeSet) Lookup(key string) (Node, bool) { if n.psMap != nil { value, ok := n.psMap.Lookup(key) if ok { - return value.(RenderableNode), true + return value.(Node), true } } - return RenderableNode{}, false + return Node{}, false } // Keys is a list of all the keys in this set. -func (n RenderableNodeSet) Keys() []string { +func (n NodeSet) Keys() []string { if n.psMap == nil { return nil } @@ -81,7 +81,7 @@ func (n RenderableNodeSet) Keys() []string { } // Size is the number of nodes in the set -func (n RenderableNodeSet) Size() int { +func (n NodeSet) Size() int { if n.psMap == nil { return 0 } @@ -90,23 +90,23 @@ func (n RenderableNodeSet) Size() int { // ForEach executes f for each node in the set. Nodes are traversed in sorted // order. -func (n RenderableNodeSet) ForEach(f func(RenderableNode)) { +func (n NodeSet) ForEach(f func(Node)) { for _, key := range n.Keys() { if val, ok := n.psMap.Lookup(key); ok { - f(val.(RenderableNode)) + f(val.(Node)) } } } // Copy is a noop -func (n RenderableNodeSet) Copy() RenderableNodeSet { +func (n NodeSet) Copy() NodeSet { return n } -func (n RenderableNodeSet) String() string { +func (n NodeSet) String() string { keys := []string{} if n.psMap == nil { - n = EmptyRenderableNodeSet + n = EmptyNodeSet } psMap := n.psMap if psMap == nil { @@ -126,9 +126,9 @@ func (n RenderableNodeSet) String() string { return buf.String() } -// DeepEqual tests equality with other RenderableNodeSets -func (n RenderableNodeSet) DeepEqual(i interface{}) bool { - d, ok := i.(RenderableNodeSet) +// DeepEqual tests equality with other NodeSets +func (n NodeSet) DeepEqual(i interface{}) bool { + d, ok := i.(NodeSet) if !ok { return false } @@ -151,20 +151,20 @@ func (n RenderableNodeSet) DeepEqual(i interface{}) bool { return equal } -func (n RenderableNodeSet) toIntermediate() []RenderableNode { - intermediate := make([]RenderableNode, 0, n.Size()) - n.ForEach(func(node RenderableNode) { +func (n NodeSet) toIntermediate() []Node { + intermediate := make([]Node, 0, n.Size()) + n.ForEach(func(node Node) { intermediate = append(intermediate, node) }) return intermediate } -func (n RenderableNodeSet) fromIntermediate(nodes []RenderableNode) RenderableNodeSet { - return MakeRenderableNodeSet(nodes...) +func (n NodeSet) fromIntermediate(nodes []Node) NodeSet { + return MakeNodeSet(nodes...) } // CodecEncodeSelf implements codec.Selfer -func (n *RenderableNodeSet) CodecEncodeSelf(encoder *codec.Encoder) { +func (n *NodeSet) CodecEncodeSelf(encoder *codec.Encoder) { if n.psMap != nil { encoder.Encode(n.toIntermediate()) } else { @@ -173,37 +173,37 @@ func (n *RenderableNodeSet) CodecEncodeSelf(encoder *codec.Encoder) { } // CodecDecodeSelf implements codec.Selfer -func (n *RenderableNodeSet) CodecDecodeSelf(decoder *codec.Decoder) { - in := []RenderableNode{} +func (n *NodeSet) CodecDecodeSelf(decoder *codec.Decoder) { + in := []Node{} if err := decoder.Decode(&in); err != nil { return } - *n = RenderableNodeSet{}.fromIntermediate(in) + *n = NodeSet{}.fromIntermediate(in) } // MarshalJSON shouldn't be used, use CodecEncodeSelf instead -func (RenderableNodeSet) MarshalJSON() ([]byte, error) { +func (NodeSet) MarshalJSON() ([]byte, error) { panic("MarshalJSON shouldn't be used, use CodecEncodeSelf instead") } // UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead -func (*RenderableNodeSet) UnmarshalJSON(b []byte) error { +func (*NodeSet) UnmarshalJSON(b []byte) error { panic("UnmarshalJSON shouldn't be used, use CodecDecodeSelf instead") } // GobEncode implements gob.Marshaller -func (n RenderableNodeSet) GobEncode() ([]byte, error) { +func (n NodeSet) GobEncode() ([]byte, error) { buf := bytes.Buffer{} err := gob.NewEncoder(&buf).Encode(n.toIntermediate()) return buf.Bytes(), err } // GobDecode implements gob.Unmarshaller -func (n *RenderableNodeSet) GobDecode(input []byte) error { - in := []RenderableNode{} +func (n *NodeSet) GobDecode(input []byte) error { + in := []Node{} if err := gob.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil { return err } - *n = RenderableNodeSet{}.fromIntermediate(in) + *n = NodeSet{}.fromIntermediate(in) return nil } diff --git a/report/node_set_test.go b/report/node_set_test.go new file mode 100644 index 0000000000..c8746a68fb --- /dev/null +++ b/report/node_set_test.go @@ -0,0 +1,249 @@ +package report_test + +import ( + "fmt" + "testing" + + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test/reflect" +) + +var benchmarkResult report.NodeSet + +type nodeSpec struct { + id string +} + +func TestMakeNodeSet(t *testing.T) { + for _, testcase := range []struct { + inputs []string + wants []string + }{ + {inputs: nil, wants: nil}, + { + inputs: []string{"a"}, + wants: []string{"a"}, + }, + { + inputs: []string{"b", "c", "a"}, + wants: []string{"a", "b", "c"}, + }, + { + inputs: []string{"a", "a", "a"}, + wants: []string{"a"}, + }, + } { + var inputs []report.Node + for _, id := range testcase.inputs { + inputs = append(inputs, report.MakeNode().WithID(id)) + } + set := report.MakeNodeSet(inputs...) + var have []string + set.ForEach(func(node report.Node) { have = append(have, node.ID) }) + if !reflect.DeepEqual(testcase.wants, have) { + t.Errorf("%#v: want %#v, have %#v", testcase.inputs, testcase.wants, have) + } + } +} + +func BenchmarkMakeNodeSet(b *testing.B) { + nodes := []report.Node{} + for i := 1000; i >= 0; i-- { + nodes = append(nodes, report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ + "a": "1", + "b": "2", + })) + } + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + benchmarkResult = report.MakeNodeSet(nodes...) + } +} + +func TestNodeSetAdd(t *testing.T) { + for _, testcase := range []struct { + input report.NodeSet + nodes []report.Node + want report.NodeSet + }{ + { + input: report.NodeSet{}, + nodes: []report.Node{}, + want: report.NodeSet{}, + }, + { + input: report.EmptyNodeSet, + nodes: []report.Node{}, + want: report.EmptyNodeSet, + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + nodes: []report.Node{}, + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.EmptyNodeSet, + nodes: []report.Node{report.MakeNode().WithID("a")}, + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + nodes: []report.Node{report.MakeNode().WithID("a")}, + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("b")), + nodes: []report.Node{ + report.MakeNode().WithID("a"), + report.MakeNode().WithID("b"), + }, + want: report.MakeNodeSet( + report.MakeNode().WithID("a"), + report.MakeNode().WithID("b"), + ), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + nodes: []report.Node{ + report.MakeNode().WithID("c"), + report.MakeNode().WithID("b"), + }, + want: report.MakeNodeSet( + report.MakeNode().WithID("a"), + report.MakeNode().WithID("b"), + report.MakeNode().WithID("c"), + ), + }, + { + input: report.MakeNodeSet( + report.MakeNode().WithID("a"), + report.MakeNode().WithID("c"), + ), + nodes: []report.Node{ + report.MakeNode().WithID("b"), + report.MakeNode().WithID("b"), + report.MakeNode().WithID("b"), + }, + want: report.MakeNodeSet( + report.MakeNode().WithID("a"), + report.MakeNode().WithID("b"), + report.MakeNode().WithID("c"), + ), + }, + } { + originalLen := testcase.input.Size() + if want, have := testcase.want, testcase.input.Add(testcase.nodes...); !reflect.DeepEqual(want, have) { + t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.nodes, want, have) + } + if testcase.input.Size() != originalLen { + t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.nodes) + } + } +} + +func BenchmarkNodeSetAdd(b *testing.B) { + n := report.EmptyNodeSet + for i := 0; i < 600; i++ { + n = n.Add( + report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ + "a": "1", + "b": "2", + }), + ) + } + + node := report.MakeNode().WithID("401.5").WithLatests(map[string]string{ + "a": "1", + "b": "2", + }) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + benchmarkResult = n.Add(node) + } +} + +func TestNodeSetMerge(t *testing.T) { + for _, testcase := range []struct { + input report.NodeSet + other report.NodeSet + want report.NodeSet + }{ + {input: report.NodeSet{}, other: report.NodeSet{}, want: report.NodeSet{}}, + {input: report.EmptyNodeSet, other: report.EmptyNodeSet, want: report.EmptyNodeSet}, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + other: report.EmptyNodeSet, + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.EmptyNodeSet, + other: report.MakeNodeSet(report.MakeNode().WithID("a")), + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + other: report.MakeNodeSet(report.MakeNode().WithID("b")), + want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("b")), + other: report.MakeNodeSet(report.MakeNode().WithID("a")), + want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a")), + other: report.MakeNodeSet(report.MakeNode().WithID("a")), + want: report.MakeNodeSet(report.MakeNode().WithID("a")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("c")), + other: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")), + want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b"), report.MakeNode().WithID("c")), + }, + { + input: report.MakeNodeSet(report.MakeNode().WithID("b")), + other: report.MakeNodeSet(report.MakeNode().WithID("a")), + want: report.MakeNodeSet(report.MakeNode().WithID("a"), report.MakeNode().WithID("b")), + }, + } { + originalLen := testcase.input.Size() + if want, have := testcase.want, testcase.input.Merge(testcase.other); !reflect.DeepEqual(want, have) { + t.Errorf("%v + %v: want %v, have %v", testcase.input, testcase.other, want, have) + } + if testcase.input.Size() != originalLen { + t.Errorf("%v + %v: modified the original input!", testcase.input, testcase.other) + } + } +} + +func BenchmarkNodeSetMerge(b *testing.B) { + n, other := report.NodeSet{}, report.NodeSet{} + for i := 0; i < 600; i++ { + n = n.Add( + report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ + "a": "1", + "b": "2", + }), + ) + } + + for i := 400; i < 1000; i++ { + other = other.Add( + report.MakeNode().WithID(fmt.Sprint(i)).WithLatests(map[string]string{ + "c": "1", + "d": "2", + }), + ) + } + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + benchmarkResult = n.Merge(other) + } +} diff --git a/report/report.go b/report/report.go index 3878e530ed..7be47d92a2 100644 --- a/report/report.go +++ b/report/report.go @@ -148,6 +148,21 @@ func (r Report) Topologies() []Topology { } } +// Topology gets a topology by name +func (r Report) Topology(name string) (Topology, bool) { + t, ok := map[string]Topology{ + Endpoint: r.Endpoint, + Process: r.Process, + Container: r.Container, + ContainerImage: r.ContainerImage, + Pod: r.Pod, + Service: r.Service, + Host: r.Host, + Overlay: r.Overlay, + }[name] + return t, ok +} + // Validate checks the report for various inconsistencies. func (r Report) Validate() error { var errs []string diff --git a/report/report_test.go b/report/report_test.go index a36ed02251..7becea8ce2 100644 --- a/report/report_test.go +++ b/report/report_test.go @@ -28,6 +28,16 @@ func TestReportTopologies(t *testing.T) { } } +func TestReportTopology(t *testing.T) { + r := report.MakeReport() + if _, ok := r.Topology(report.Container); !ok { + t.Errorf("Expected %s topology to be found", report.Container) + } + if _, ok := r.Topology("foo"); ok { + t.Errorf("Expected %s topology not to be found", "foo") + } +} + func TestNode(t *testing.T) { { node := report.MakeNode().WithLatests(map[string]string{ diff --git a/report/topology.go b/report/topology.go index 2cd890db43..46af52b436 100644 --- a/report/topology.go +++ b/report/topology.go @@ -78,6 +78,16 @@ func (n Nodes) Merge(other Nodes) Nodes { return cp } +// Prune returns a copy of the Nodes with all information not strictly +// necessary for rendering nodes and edges in the UI cut away. +func (n Nodes) Prune() Nodes { + result := Nodes{} + for id, node := range n { + result[id] = node.Prune() + } + return result +} + // Validate checks the topology for various inconsistencies. func (t Topology) Validate() error { errs := []string{} diff --git a/test/fixture/report_fixture.go b/test/fixture/report_fixture.go index edcfec2ab9..4eacbc67ab 100644 --- a/test/fixture/report_fixture.go +++ b/test/fixture/report_fixture.go @@ -8,7 +8,7 @@ import ( "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" - "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/render/detailed" "github.com/weaveworks/scope/report" ) @@ -121,7 +121,7 @@ var ( // Node 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.MakeNode().WithLatests(map[string]string{ + Client54001NodeID: report.MakeNode().WithID(Client54001NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: ClientIP, endpoint.Port: ClientPort54001, process.PID: Client1PID, @@ -132,7 +132,7 @@ var ( EgressByteCount: newu64(100), }), - Client54002NodeID: report.MakeNode().WithLatests(map[string]string{ + Client54002NodeID: report.MakeNode().WithID(Client54002NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: ClientIP, endpoint.Port: ClientPort54002, process.PID: Client2PID, @@ -143,7 +143,7 @@ var ( EgressByteCount: newu64(200), }), - Server80NodeID: report.MakeNode().WithLatests(map[string]string{ + Server80NodeID: report.MakeNode().WithID(Server80NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: ServerIP, endpoint.Port: ServerPort, process.PID: ServerPID, @@ -151,7 +151,7 @@ var ( endpoint.Procspied: True, }), - NonContainerNodeID: report.MakeNode().WithLatests(map[string]string{ + NonContainerNodeID: report.MakeNode().WithID(NonContainerNodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: ServerIP, endpoint.Port: NonContainerClientPort, process.PID: NonContainerPID, @@ -160,7 +160,7 @@ var ( }).WithAdjacent(GoogleEndpointNodeID), // Probe pseudo nodes - UnknownClient1NodeID: report.MakeNode().WithLatests(map[string]string{ + UnknownClient1NodeID: report.MakeNode().WithID(UnknownClient1NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: UnknownClient1IP, endpoint.Port: UnknownClient1Port, endpoint.Procspied: True, @@ -169,7 +169,7 @@ var ( EgressByteCount: newu64(300), }), - UnknownClient2NodeID: report.MakeNode().WithLatests(map[string]string{ + UnknownClient2NodeID: report.MakeNode().WithID(UnknownClient2NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: UnknownClient2IP, endpoint.Port: UnknownClient2Port, endpoint.Procspied: True, @@ -178,7 +178,7 @@ var ( EgressByteCount: newu64(400), }), - UnknownClient3NodeID: report.MakeNode().WithLatests(map[string]string{ + UnknownClient3NodeID: report.MakeNode().WithID(UnknownClient3NodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: UnknownClient3IP, endpoint.Port: UnknownClient3Port, endpoint.Procspied: True, @@ -187,7 +187,7 @@ var ( EgressByteCount: newu64(500), }), - RandomClientNodeID: report.MakeNode().WithLatests(map[string]string{ + RandomClientNodeID: report.MakeNode().WithID(RandomClientNodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: RandomClientIP, endpoint.Port: RandomClientPort, endpoint.Procspied: True, @@ -196,7 +196,7 @@ var ( EgressByteCount: newu64(600), }), - GoogleEndpointNodeID: report.MakeNode().WithLatests(map[string]string{ + GoogleEndpointNodeID: report.MakeNode().WithID(GoogleEndpointNodeID).WithTopology(report.Endpoint).WithLatests(map[string]string{ endpoint.Addr: GoogleIP, endpoint.Port: GooglePort, endpoint.Procspied: True, @@ -267,17 +267,17 @@ var ( docker.MemoryUsage: ClientContainerMemoryMetric, }), ServerContainerNodeID: report.MakeNodeWith(map[string]string{ - docker.ContainerID: ServerContainerID, - docker.ContainerName: "task-name-5-server-aceb93e2f2b797caba01", - docker.ContainerState: docker.StateRunning, - docker.ImageID: ServerContainerImageID, - report.HostNodeID: ServerHostNodeID, - docker.LabelPrefix + render.AmazonECSContainerNameLabel: "server", - docker.LabelPrefix + "foo1": "bar1", - docker.LabelPrefix + "foo2": "bar2", - docker.LabelPrefix + "io.kubernetes.pod.name": ServerPodID, - kubernetes.PodID: ServerPodID, - kubernetes.Namespace: KubernetesNamespace, + docker.ContainerID: ServerContainerID, + docker.ContainerName: "task-name-5-server-aceb93e2f2b797caba01", + docker.ContainerState: docker.StateRunning, + docker.ImageID: ServerContainerImageID, + report.HostNodeID: ServerHostNodeID, + docker.LabelPrefix + detailed.AmazonECSContainerNameLabel: "server", + docker.LabelPrefix + "foo1": "bar1", + docker.LabelPrefix + "foo2": "bar2", + docker.LabelPrefix + "io.kubernetes.pod.name": ServerPodID, + kubernetes.PodID: ServerPodID, + kubernetes.Namespace: KubernetesNamespace, }).WithID(ServerContainerNodeID).WithTopology(report.Container).WithParents(report.EmptySets. Add("host", report.MakeStringSet(ServerHostNodeID)). Add("container_image", report.MakeStringSet(ServerContainerImageNodeID)).