diff --git a/app/api_topologies.go b/app/api_topologies.go index e152dc12ad..9c400e34e4 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -26,9 +26,9 @@ func init() { ID: "system", Default: "application", Options: []APITopologyOption{ - {"system", "System services", render.FilterApplication}, - {"application", "Application services", render.FilterSystem}, - {"both", "Both", render.FilterNoop}, + {"system", "System services", render.IsSystem}, + {"application", "Application services", render.IsApplication}, + {"both", "Both", render.Noop}, }, }, } @@ -38,9 +38,9 @@ func init() { ID: "system", Default: "application", Options: []APITopologyOption{ - {"system", "System pods", render.FilterApplication}, - {"application", "Application pods", render.FilterSystem}, - {"both", "Both", render.FilterNoop}, + {"system", "System pods", render.IsSystem}, + {"application", "Application pods", render.IsApplication}, + {"both", "Both", render.Noop}, }, }, } @@ -50,18 +50,18 @@ func init() { ID: "system", Default: "application", Options: []APITopologyOption{ - {"system", "System containers", render.FilterApplication}, - {"application", "Application containers", render.FilterSystem}, - {"both", "Both", render.FilterNoop}, + {"system", "System containers", render.IsSystem}, + {"application", "Application containers", render.IsApplication}, + {"both", "Both", render.Noop}, }, }, { ID: "stopped", Default: "running", Options: []APITopologyOption{ - {"stopped", "Stopped containers", render.FilterRunning}, - {"running", "Running containers", render.FilterStopped}, - {"both", "Both", render.FilterNoop}, + {"stopped", "Stopped containers", render.IsStopped}, + {"running", "Running containers", render.IsRunning}, + {"both", "Both", render.Noop}, }, }, } @@ -73,7 +73,7 @@ func init() { Options: []APITopologyOption{ // Show the user why there are filtered nodes in this view. // Don't give them the option to show those nodes. - {"hide", "Unconnected nodes hidden", render.FilterNoop}, + {"hide", "Unconnected nodes hidden", render.Noop}, }, }, } @@ -181,7 +181,7 @@ type APITopologyOption struct { Value string `json:"value"` Label string `json:"label"` - decorator func(render.Renderer) render.Renderer + filter render.FilterFunc } type topologyStats struct { @@ -247,31 +247,31 @@ func (r *registry) makeTopologyList(rep Reporter) CtxHandlerFunc { func (r *registry) renderTopologies(rpt report.Report, req *http.Request) []APITopologyDesc { topologies := []APITopologyDesc{} r.walk(func(desc APITopologyDesc) { - renderer := renderedForRequest(req, desc) - desc.Stats = decorateWithStats(rpt, renderer) + renderer, decorator := renderedForRequest(req, desc) + desc.Stats = decorateWithStats(rpt, renderer, decorator) for i := range desc.SubTopologies { - renderer := renderedForRequest(req, desc.SubTopologies[i]) - desc.SubTopologies[i].Stats = decorateWithStats(rpt, renderer) + renderer, decorator := renderedForRequest(req, desc.SubTopologies[i]) + desc.SubTopologies[i].Stats = decorateWithStats(rpt, renderer, decorator) } topologies = append(topologies, desc) }) return topologies } -func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStats { +func decorateWithStats(rpt report.Report, renderer render.Renderer, decorator render.Decorator) topologyStats { var ( nodes int realNodes int edges int ) - for _, n := range renderer.Render(rpt) { + for _, n := range renderer.Render(rpt, decorator) { nodes++ if n.Topology != render.Pseudo { realNodes++ } edges += len(n.Adjacency) } - renderStats := renderer.Stats(rpt) + renderStats := renderer.Stats(rpt, decorator) return topologyStats{ NodeCount: nodes, NonpseudoNodeCount: realNodes, @@ -280,20 +280,26 @@ func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStat } } -func renderedForRequest(r *http.Request, topology APITopologyDesc) render.Renderer { - renderer := topology.renderer +func renderedForRequest(r *http.Request, topology APITopologyDesc) (render.Renderer, render.Decorator) { + var filters []render.FilterFunc for _, group := range topology.Options { value := r.FormValue(group.ID) for _, opt := range group.Options { if (value == "" && group.Default == opt.Value) || (opt.Value != "" && opt.Value == value) { - renderer = opt.decorator(renderer) + filters = append(filters, opt.filter) } } } - return renderer + return topology.renderer, func(renderer render.Renderer) render.Renderer { + return render.MakeFilter(render.ComposeFilterFuncs(filters...), renderer) + } } -type reportRenderHandler func(context.Context, Reporter, render.Renderer, http.ResponseWriter, *http.Request) +type reportRenderHandler func( + context.Context, + Reporter, render.Renderer, render.Decorator, + http.ResponseWriter, *http.Request, +) func (r *registry) captureRenderer(rep Reporter, f reportRenderHandler) CtxHandlerFunc { return func(ctx context.Context, w http.ResponseWriter, req *http.Request) { @@ -302,18 +308,7 @@ func (r *registry) captureRenderer(rep Reporter, f reportRenderHandler) CtxHandl http.NotFound(w, req) return } - renderer := renderedForRequest(req, topology) - f(ctx, rep, renderer, w, req) - } -} - -func (r *registry) captureRendererWithoutFilters(rep Reporter, f reportRenderHandler) CtxHandlerFunc { - return func(ctx context.Context, w http.ResponseWriter, req *http.Request) { - topology, ok := r.get(mux.Vars(req)["topology"]) - if !ok { - http.NotFound(w, req) - return - } - f(ctx, rep, topology.renderer, w, req) + renderer, decorator := renderedForRequest(req, topology) + f(ctx, rep, renderer, decorator, w, req) } } diff --git a/app/api_topology.go b/app/api_topology.go index 57796d90f8..d9ecd73788 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -28,19 +28,27 @@ type APINode struct { } // Full topology. -func handleTopology(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) { +func handleTopology( + ctx context.Context, + rep Reporter, renderer render.Renderer, decorator render.Decorator, + w http.ResponseWriter, r *http.Request, +) { report, err := rep.Report(ctx) if err != nil { respondWith(w, http.StatusInternalServerError, err.Error()) return } respondWith(w, http.StatusOK, APITopology{ - Nodes: detailed.Summaries(report, renderer.Render(report)), + Nodes: detailed.Summaries(report, renderer.Render(report, decorator)), }) } // Websocket for the full topology. This route overlaps with the next. -func handleWs(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) { +func handleWs( + ctx context.Context, + rep Reporter, renderer render.Renderer, decorator render.Decorator, + w http.ResponseWriter, r *http.Request, +) { if err := r.ParseForm(); err != nil { respondWith(w, http.StatusInternalServerError, err.Error()) return @@ -53,17 +61,21 @@ func handleWs(ctx context.Context, rep Reporter, renderer render.Renderer, w htt return } } - handleWebsocket(ctx, w, r, rep, renderer, loop) + handleWebsocket(ctx, w, r, rep, renderer, decorator, loop) } // Individual nodes. -func handleNode(ctx context.Context, rep Reporter, renderer render.Renderer, w http.ResponseWriter, r *http.Request) { +func handleNode( + ctx context.Context, + rep Reporter, renderer render.Renderer, _ render.Decorator, + w http.ResponseWriter, r *http.Request, +) { var ( vars = mux.Vars(r) topologyID = vars["topology"] nodeID = vars["id"] report, err = rep.Report(ctx) - rendered = renderer.Render(report) + rendered = renderer.Render(report, render.FilterNoop) node, ok = rendered[nodeID] ) if err != nil { @@ -83,6 +95,7 @@ func handleWebsocket( r *http.Request, rep Reporter, renderer render.Renderer, + decorator render.Decorator, loop time.Duration, ) { conn, err := xfer.Upgrade(w, r, nil) @@ -119,7 +132,7 @@ func handleWebsocket( log.Errorf("Error generating report: %v", err) return } - newTopo := detailed.Summaries(report, renderer.Render(report)) + newTopo := detailed.Summaries(report, renderer.Render(report, decorator)) diff := detailed.TopoDiff(previousTopo, newTopo) previousTopo = newTopo diff --git a/app/router.go b/app/router.go index 3f5982c8a3..d2ca136dda 100644 --- a/app/router.go +++ b/app/router.go @@ -93,7 +93,7 @@ func RegisterTopologyRoutes(router *mux.Router, r Reporter) { get.HandleFunc("/api/topology/{topology}/ws", requestContextDecorator(topologyRegistry.captureRenderer(r, handleWs))) // NB not gzip! get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc( - gzipHandler(requestContextDecorator(topologyRegistry.captureRendererWithoutFilters(r, handleNode)))) + gzipHandler(requestContextDecorator(topologyRegistry.captureRenderer(r, handleNode)))) get.HandleFunc("/api/report", gzipHandler(requestContextDecorator(makeRawReportHandler(r)))) get.HandleFunc("/api/probes", diff --git a/experimental/graphviz/render.go b/experimental/graphviz/render.go index 511184b6fd..5433db826c 100644 --- a/experimental/graphviz/render.go +++ b/experimental/graphviz/render.go @@ -19,5 +19,5 @@ func renderTo(rpt report.Report, topology string) (detailed.NodeSummaries, error if !ok { return detailed.NodeSummaries{}, fmt.Errorf("unknown topology %v", topology) } - return detailed.Summaries(rpt, renderer.Render(rpt)), nil + return detailed.Summaries(rpt, renderer.Render(rpt, render.FilterNoop)), nil } diff --git a/render/benchmark_test.go b/render/benchmark_test.go index 05889b21ed..ecf00d3288 100644 --- a/render/benchmark_test.go +++ b/render/benchmark_test.go @@ -38,22 +38,31 @@ func BenchmarkContainerWithImageNameRender(b *testing.B) { func BenchmarkContainerWithImageNameStats(b *testing.B) { benchmarkStats(b, render.ContainerWithImageNameRenderer) } -func BenchmarkContainerImageRender(b *testing.B) { benchmarkRender(b, render.ContainerImageRenderer) } -func BenchmarkContainerImageStats(b *testing.B) { benchmarkStats(b, render.ContainerImageRenderer) } +func BenchmarkContainerImageRender(b *testing.B) { + benchmarkRender(b, render.ContainerImageRenderer) +} +func BenchmarkContainerImageStats(b *testing.B) { + benchmarkStats(b, render.ContainerImageRenderer) +} func BenchmarkContainerHostnameRender(b *testing.B) { benchmarkRender(b, render.ContainerHostnameRenderer) } func BenchmarkContainerHostnameStats(b *testing.B) { benchmarkStats(b, render.ContainerHostnameRenderer) } -func BenchmarkHostRender(b *testing.B) { benchmarkRender(b, render.HostRenderer) } -func BenchmarkHostStats(b *testing.B) { benchmarkStats(b, render.HostRenderer) } -func BenchmarkPodRender(b *testing.B) { benchmarkRender(b, render.PodRenderer) } -func BenchmarkPodStats(b *testing.B) { benchmarkStats(b, render.PodRenderer) } -func BenchmarkPodServiceRender(b *testing.B) { benchmarkRender(b, render.PodServiceRenderer) } -func BenchmarkPodServiceStats(b *testing.B) { benchmarkStats(b, render.PodServiceRenderer) } +func BenchmarkHostRender(b *testing.B) { benchmarkRender(b, render.HostRenderer) } +func BenchmarkHostStats(b *testing.B) { benchmarkStats(b, render.HostRenderer) } +func BenchmarkPodRender(b *testing.B) { benchmarkRender(b, render.PodRenderer) } +func BenchmarkPodStats(b *testing.B) { benchmarkStats(b, render.PodRenderer) } +func BenchmarkPodServiceRender(b *testing.B) { + benchmarkRender(b, render.PodServiceRenderer) +} +func BenchmarkPodServiceStats(b *testing.B) { + benchmarkStats(b, render.PodServiceRenderer) +} func benchmarkRender(b *testing.B, r render.Renderer) { + report, err := loadReport() if err != nil { b.Fatal(err) @@ -65,7 +74,7 @@ func benchmarkRender(b *testing.B, r render.Renderer) { b.StopTimer() render.ResetCache() b.StartTimer() - benchmarkRenderResult = r.Render(report) + benchmarkRenderResult = r.Render(report, render.FilterNoop) if len(benchmarkRenderResult) == 0 { b.Errorf("Rendered topology contained no nodes") } @@ -85,7 +94,7 @@ func benchmarkStats(b *testing.B, r render.Renderer) { b.StopTimer() render.ResetCache() b.StartTimer() - benchmarkStatsResult = r.Stats(report) + benchmarkStatsResult = r.Stats(report, render.FilterNoop) } } diff --git a/render/container.go b/render/container.go index c15f8762e6..55fc9fc811 100644 --- a/render/container.go +++ b/render/container.go @@ -26,14 +26,14 @@ const ( // NB We only want processes in container _or_ processes with network connections // but we need to be careful to ensure we only include each edge once, by only // including the ProcessRenderer once. -var ContainerRenderer = MakeSilentFilter( +var ContainerRenderer = MakeFilter( func(n report.Node) bool { // Drop deleted containers state, ok := n.Latest.Lookup(docker.ContainerState) return !ok || state != docker.StateDeleted }, MakeReduce( - MakeSilentFilter( + MakeFilter( func(n report.Node) bool { // Drop unconnected pseudo nodes (could appear due to filtering) _, isConnected := n.Latest.Lookup(IsConnected) @@ -49,7 +49,7 @@ var ContainerRenderer = MakeSilentFilter( // We need to be careful to ensure we only include each edge once. Edges brought in // by the above renders will have a pid, so its enough to filter out any nodes with // pids. - SilentFilterUnconnected(MakeMap( + FilterUnconnected(MakeMap( MapIP2Container, MakeReduce( MakeMap( @@ -73,9 +73,9 @@ 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) report.Nodes { - containers := r.Renderer.Render(rpt) - hosts := SelectHost.Render(rpt) +func (r containerWithHostIPsRenderer) Render(rpt report.Report, dct Decorator) report.Nodes { + containers := r.Renderer.Render(rpt, dct) + hosts := SelectHost.Render(rpt, dct) outputs := report.Nodes{} for id, c := range containers { @@ -116,9 +116,9 @@ 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) report.Nodes { - containers := r.Renderer.Render(rpt) - images := SelectContainerImage.Render(rpt) +func (r containerWithImageNameRenderer) Render(rpt report.Report, dct Decorator) report.Nodes { + containers := r.Renderer.Render(rpt, dct) + images := SelectContainerImage.Render(rpt, dct) outputs := report.Nodes{} for id, c := range containers { @@ -140,23 +140,38 @@ func (r containerWithImageNameRenderer) Render(rpt report.Report) report.Nodes { // ContainerWithImageNameRenderer is a Renderer which produces a container // graph where the ranks are the image names, not their IDs -var ContainerWithImageNameRenderer = containerWithImageNameRenderer{ContainerWithHostIPsRenderer} +var ContainerWithImageNameRenderer = ApplyDecorators(containerWithImageNameRenderer{ContainerWithHostIPsRenderer}) // ContainerImageRenderer is a Renderer which produces a renderable container // image graph by merging the container graph and the container image topology. -var ContainerImageRenderer = MakeReduce( - MakeMap( - MapContainer2ContainerImage, - ContainerRenderer, +var ContainerImageRenderer = FilterEmpty(report.Container, + MakeReduce( + MakeMap( + MapContainer2ContainerImage, + ContainerWithImageNameRenderer, + ), + SelectContainerImage, ), - SelectContainerImage, ) // ContainerHostnameRenderer is a Renderer which produces a renderable container // by hostname graph.. -var ContainerHostnameRenderer = MakeMap( - MapContainer2Hostname, - ContainerRenderer, +var ContainerHostnameRenderer = FilterEmpty(report.Container, + MakeReduce( + MakeMap( + MapContainer2Hostname, + ContainerWithImageNameRenderer, + ), + // Grab *all* the hostnames, so we can count the number which were empty + // for accurate stats. + MakeMap( + MapToEmpty, + MakeMap( + MapContainer2Hostname, + ContainerRenderer, + ), + ), + ), ) var portMappingMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):([0-9]+)->([0-9]+)/tcp`) @@ -364,3 +379,9 @@ func ImageNameWithoutVersion(name string) string { parts = strings.SplitN(name, ":", 2) return parts[0] } + +// MapToEmpty removes all the attributes, children, etc, of a node. Useful when +// we just want to count the presence of nodes. +func MapToEmpty(n report.Node, _ report.Networks) report.Nodes { + return report.Nodes{n.ID: report.MakeNode(n.ID).WithTopology(n.Topology)} +} diff --git a/render/container_test.go b/render/container_test.go index 5c2fde89d0..75a60ae598 100644 --- a/render/container_test.go +++ b/render/container_test.go @@ -47,8 +47,8 @@ func testMap(t *testing.T, f render.MapFunc, input testcase) { } func TestContainerRenderer(t *testing.T) { - have := render.ContainerRenderer.Render(fixture.Report).Prune() - want := expected.RenderedContainers.Prune() + have := Prune(render.ContainerWithImageNameRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedContainers) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -61,8 +61,8 @@ func TestContainerFilterRenderer(t *testing.T) { input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{ docker.LabelPrefix + "works.weave.role": "system", }) - have := render.FilterSystem(render.ContainerRenderer).Render(input).Prune() - want := expected.RenderedContainers.Copy().Prune() + have := Prune(render.ContainerWithImageNameRenderer.Render(input, render.FilterApplication)) + want := Prune(expected.RenderedContainers.Copy()) delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -74,7 +74,7 @@ func TestContainerWithHostIPsRenderer(t *testing.T) { input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{ docker.ContainerNetworkMode: "host", }) - nodes := render.ContainerWithHostIPsRenderer.Render(input) + nodes := render.ContainerWithHostIPsRenderer.Render(input, render.FilterNoop) // Test host network nodes get the host IPs added. haveNode, ok := nodes[fixture.ClientContainerNodeID] @@ -91,9 +91,71 @@ func TestContainerWithHostIPsRenderer(t *testing.T) { } } +func TestContainerHostnameRenderer(t *testing.T) { + have := Prune(render.ContainerHostnameRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedContainerHostnames) + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + +func TestContainerHostnameFilterRenderer(t *testing.T) { + // add a system container into the topology and ensure + // it is filtered out correctly. + input := fixture.Report.Copy() + + clientContainer2ID := "f6g7h8i9j1" + clientContainer2NodeID := report.MakeContainerNodeID(clientContainer2ID) + input.Container.AddNode(report.MakeNodeWith(clientContainer2NodeID, map[string]string{ + docker.LabelPrefix + "works.weave.role": "system", + docker.ContainerHostname: fixture.ClientContainerHostname, + report.HostNodeID: fixture.ClientHostNodeID, + }). + WithParents(report.EmptySets. + Add("host", report.MakeStringSet(fixture.ClientHostNodeID)), + ).WithTopology(report.Container)) + + have := Prune(render.ContainerHostnameRenderer.Render(input, render.FilterApplication)) + want := Prune(expected.RenderedContainerHostnames) + // Test works by virtue of the RenderedContainerHostname only having a container + // counter == 1 + + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + func TestContainerImageRenderer(t *testing.T) { - have := render.ContainerImageRenderer.Render(fixture.Report).Prune() - want := expected.RenderedContainerImages.Prune() + have := Prune(render.ContainerImageRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedContainerImages) + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + +func TestContainerImageFilterRenderer(t *testing.T) { + // add a system container into the topology and ensure + // it is filtered out correctly. + input := fixture.Report.Copy() + + clientContainer2ID := "f6g7h8i9j1" + clientContainer2NodeID := report.MakeContainerNodeID(clientContainer2ID) + input.Container.AddNode(report.MakeNodeWith(clientContainer2NodeID, map[string]string{ + docker.LabelPrefix + "works.weave.role": "system", + + docker.ImageID: fixture.ClientContainerImageID, + docker.ImageName: fixture.ClientContainerImageName, + report.HostNodeID: fixture.ClientHostNodeID, + }). + WithParents(report.EmptySets. + Add("host", report.MakeStringSet(fixture.ClientHostNodeID)), + ).WithTopology(report.ContainerImage)) + + have := Prune(render.ContainerImageRenderer.Render(input, render.FilterApplication)) + want := Prune(expected.RenderedContainerImages.Copy()) + // Test works by virtue of the RenderedContainerImage only having a container + // counter == 1 + if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index 78c0e1a4a8..17b6b7d09d 100644 --- a/render/detailed/node_test.go +++ b/render/detailed/node_test.go @@ -16,7 +16,7 @@ import ( ) func child(t *testing.T, r render.Renderer, id string) detailed.NodeSummary { - s, ok := detailed.MakeNodeSummary(fixture.Report, r.Render(fixture.Report)[id]) + s, ok := detailed.MakeNodeSummary(fixture.Report, r.Render(fixture.Report, render.FilterNoop)[id]) if !ok { t.Fatalf("Expected node %s to be summarizable, but wasn't", id) } @@ -24,7 +24,7 @@ func child(t *testing.T, r render.Renderer, id string) detailed.NodeSummary { } func TestMakeDetailedHostNode(t *testing.T) { - renderableNodes := render.HostRenderer.Render(fixture.Report) + renderableNodes := render.HostRenderer.Render(fixture.Report, render.FilterNoop) renderableNode := renderableNodes[fixture.ClientHostNodeID] have := detailed.MakeNode("hosts", fixture.Report, renderableNodes, renderableNode) @@ -171,7 +171,7 @@ func TestMakeDetailedHostNode(t *testing.T) { func TestMakeDetailedContainerNode(t *testing.T) { id := fixture.ServerContainerNodeID - renderableNodes := render.ContainerRenderer.Render(fixture.Report) + renderableNodes := render.ContainerRenderer.Render(fixture.Report, render.FilterNoop) renderableNode, ok := renderableNodes[id] if !ok { t.Fatalf("Node not found: %s", id) @@ -298,14 +298,14 @@ func TestMakeDetailedContainerNode(t *testing.T) { func TestMakeDetailedPodNode(t *testing.T) { id := fixture.ServerPodNodeID - renderableNodes := render.PodRenderer.Render(fixture.Report) + renderableNodes := render.PodRenderer.Render(fixture.Report, render.FilterNoop) renderableNode, ok := renderableNodes[id] if !ok { t.Fatalf("Node not found: %s", id) } have := detailed.MakeNode("pods", fixture.Report, renderableNodes, renderableNode) - containerNodeSummary := child(t, render.ContainerRenderer, fixture.ServerContainerNodeID) + containerNodeSummary := child(t, render.ContainerWithImageNameRenderer, fixture.ServerContainerNodeID) serverProcessNodeSummary := child(t, render.ProcessRenderer, fixture.ServerProcessNodeID) serverProcessNodeSummary.Linkable = true // Temporary workaround for: https://github.com/weaveworks/scope/issues/1295 want := detailed.Node{ diff --git a/render/detailed/parents_test.go b/render/detailed/parents_test.go index 7a21b18bac..5351acdb61 100644 --- a/render/detailed/parents_test.go +++ b/render/detailed/parents_test.go @@ -20,30 +20,30 @@ func TestParents(t *testing.T) { }{ { name: "Node accidentally tagged with itself", - node: render.HostRenderer.Render(fixture.Report)[fixture.ClientHostNodeID].WithParents( + node: render.HostRenderer.Render(fixture.Report, render.FilterNoop)[fixture.ClientHostNodeID].WithParents( report.EmptySets.Add(report.Host, report.MakeStringSet(fixture.ClientHostNodeID)), ), want: nil, }, { - node: render.HostRenderer.Render(fixture.Report)[fixture.ClientHostNodeID], + node: render.HostRenderer.Render(fixture.Report, render.FilterNoop)[fixture.ClientHostNodeID], want: nil, }, { - node: render.ContainerImageRenderer.Render(fixture.Report)[fixture.ClientContainerImageNodeID], + node: render.ContainerImageRenderer.Render(fixture.Report, render.FilterNoop)[fixture.ClientContainerImageNodeID], want: []detailed.Parent{ {ID: fixture.ClientHostNodeID, Label: fixture.ClientHostName, TopologyID: "hosts"}, }, }, { - node: render.ContainerRenderer.Render(fixture.Report)[fixture.ClientContainerNodeID], + node: render.ContainerRenderer.Render(fixture.Report, render.FilterNoop)[fixture.ClientContainerNodeID], want: []detailed.Parent{ {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], + node: render.ProcessRenderer.Render(fixture.Report, render.FilterNoop)[fixture.ClientProcess1NodeID], want: []detailed.Parent{ {ID: fixture.ClientContainerNodeID, Label: fixture.ClientContainerName, TopologyID: "containers"}, {ID: fixture.ClientContainerImageNodeID, Label: fixture.ClientContainerImageName, TopologyID: "containers-by-image"}, diff --git a/render/detailed/summary_test.go b/render/detailed/summary_test.go index 93e544e674..cfe1021ec6 100644 --- a/render/detailed/summary_test.go +++ b/render/detailed/summary_test.go @@ -21,7 +21,7 @@ import ( func TestSummaries(t *testing.T) { { // Just a convenient source of some rendered nodes - have := detailed.Summaries(fixture.Report, render.ProcessRenderer.Render(fixture.Report)) + have := detailed.Summaries(fixture.Report, render.ProcessRenderer.Render(fixture.Report, render.FilterNoop)) // The ids of the processes rendered above expectedIDs := []string{ fixture.ClientProcess1NodeID, @@ -51,7 +51,7 @@ func TestSummaries(t *testing.T) { input := fixture.Report.Copy() input.Process.Nodes[fixture.ClientProcess1NodeID] = input.Process.Nodes[fixture.ClientProcess1NodeID].WithMetrics(report.Metrics{process.CPUUsage: metric}) - have := detailed.Summaries(input, render.ProcessRenderer.Render(input)) + have := detailed.Summaries(input, render.ProcessRenderer.Render(input, render.FilterNoop)) node, ok := have[fixture.ClientProcess1NodeID] if !ok { diff --git a/render/expected/expected.go b/render/expected/expected.go index 88a69fc308..351f19c993 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -27,15 +27,16 @@ var ( return n } } - pseudo = node(render.Pseudo) - endpoint = node(report.Endpoint) - processNode = node(report.Process) - processNameNode = node(render.MakeGroupNodeTopology(report.Process, process.Name)) - container = node(report.Container) - containerImage = node(report.ContainerImage) - pod = node(report.Pod) - service = node(report.Service) - hostNode = node(report.Host) + pseudo = node(render.Pseudo) + endpoint = node(report.Endpoint) + processNode = node(report.Process) + processNameNode = node(render.MakeGroupNodeTopology(report.Process, process.Name)) + container = node(report.Container) + containerHostnameNode = node(render.MakeGroupNodeTopology(report.Container, docker.ContainerHostname)) + 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) @@ -176,6 +177,37 @@ var ( render.OutgoingInternetID: theOutgoingInternetNode, } + RenderedContainerHostnames = report.Nodes{ + fixture.ClientContainerHostname: containerHostnameNode(fixture.ClientContainerHostname, fixture.ServerContainerHostname). + WithLatests(map[string]string{ + docker.ContainerHostname: fixture.ClientContainerHostname, + }). + 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.ServerContainerHostname: containerHostnameNode(fixture.ServerContainerHostname). + WithLatests(map[string]string{ + docker.ContainerHostname: fixture.ServerContainerHostname, + }). + WithChildren(report.MakeNodeSet( + RenderedEndpoints[fixture.Server80NodeID], + RenderedProcesses[fixture.ServerProcessNodeID], + RenderedContainers[fixture.ServerContainerNodeID], + )), + + uncontainedServerID: uncontainedServerNode, + render.IncomingInternetID: theIncomingInternetNode(fixture.ServerContainerHostname), + render.OutgoingInternetID: theOutgoingInternetNode, + } + RenderedContainerImages = report.Nodes{ fixture.ClientContainerImageNodeID: containerImage(fixture.ClientContainerImageNodeID, fixture.ServerContainerImageNodeID). WithLatests(map[string]string{ diff --git a/render/filters.go b/render/filters.go index 61839292f5..9dc4ab735e 100644 --- a/render/filters.go +++ b/render/filters.go @@ -19,8 +19,8 @@ type CustomRenderer struct { } // Render implements Renderer -func (c CustomRenderer) Render(rpt report.Report) report.Nodes { - return c.RenderFunc(c.Renderer.Render(rpt)) +func (c CustomRenderer) Render(rpt report.Report, dct Decorator) report.Nodes { + return c.RenderFunc(c.Renderer.Render(rpt, dct)) } // ColorConnected colors nodes with the IsConnected key if @@ -55,41 +55,46 @@ func ColorConnected(r Renderer) Renderer { } } +// FilterFunc is the function type used by Filters +type FilterFunc func(report.Node) bool + +// ComposeFilterFuncs composes filterfuncs into a single FilterFunc checking all. +func ComposeFilterFuncs(fs ...FilterFunc) FilterFunc { + return func(n report.Node) bool { + for _, f := range fs { + if !f(n) { + return false + } + } + return true + } +} + // Filter removes nodes from a view based on a predicate. type Filter struct { Renderer - FilterFunc func(report.Node) bool - Silent bool // true means we don't report stats for how many are filtered + FilterFunc FilterFunc } // MakeFilter makes a new Filter. -func MakeFilter(f func(report.Node) bool, r Renderer) Renderer { - return Memoise(&Filter{ - Renderer: r, - FilterFunc: f, - }) -} - -// MakeSilentFilter makes a new Filter which does not report how many nodes it filters in Stats. -func MakeSilentFilter(f func(report.Node) bool, r Renderer) Renderer { +func MakeFilter(f FilterFunc, r Renderer) Renderer { return Memoise(&Filter{ Renderer: r, FilterFunc: f, - Silent: true, }) } // Render implements Renderer -func (f *Filter) Render(rpt report.Report) report.Nodes { - nodes, _ := f.render(rpt) +func (f *Filter) Render(rpt report.Report, dct Decorator) report.Nodes { + nodes, _ := f.render(rpt, dct) return nodes } -func (f *Filter) render(rpt report.Report) (report.Nodes, int) { +func (f *Filter) render(rpt report.Report, dct Decorator) (report.Nodes, int) { output := report.Nodes{} inDegrees := map[string]int{} filtered := 0 - for id, node := range f.Renderer.Render(rpt) { + for id, node := range f.Renderer.Render(rpt, dct) { if f.FilterFunc(node) { output[id] = node inDegrees[id] = 0 @@ -126,14 +131,13 @@ func (f *Filter) render(rpt report.Report) (report.Nodes, int) { return output, filtered } -// Stats implements Renderer -func (f Filter) Stats(rpt report.Report) Stats { - var upstream = f.Renderer.Stats(rpt) - if !f.Silent { - _, filtered := f.render(rpt) - upstream.FilteredNodes += filtered - } - return upstream +// Stats implements Renderer. General logic is to take the first (i.e. +// highest-level) stats we find, so upstream stats are ignored. This means that +// if we want to count the stats from multiple filters we need to compose their +// FilterFuncs, into a single Filter. +func (f Filter) Stats(rpt report.Report, dct Decorator) Stats { + _, filtered := f.render(rpt, dct) + return Stats{FilteredNodes: filtered} } // IsConnected is the key added to Node.Metadata by ColorConnected @@ -142,7 +146,7 @@ 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(report.Node) bool) func(report.Node) bool { +func Complement(f FilterFunc) FilterFunc { return func(node report.Node) bool { return !f(node) } } @@ -169,27 +173,11 @@ func FilterUnconnected(r Renderer) Renderer { ) } -// SilentFilterUnconnected produces a renderer that filters unconnected nodes -// from the given renderer; nodes filtered by this are not reported in stats. -func SilentFilterUnconnected(r Renderer) Renderer { - return MakeSilentFilter( - func(node report.Node) bool { - _, ok := node.Latest.Lookup(IsConnected) - return ok - }, - ColorConnected(r), - ) -} +// Noop allows all nodes through +func Noop(_ report.Node) bool { return true } // FilterNoop does nothing. -func FilterNoop(in Renderer) Renderer { - return in -} - -// FilterStopped filters out stopped containers. -func FilterStopped(r Renderer) Renderer { - return MakeFilter(IsRunning, r) -} +func FilterNoop(r Renderer) Renderer { return r } // IsRunning checks if the node is a running docker container func IsRunning(n report.Node) bool { @@ -197,14 +185,22 @@ func IsRunning(n report.Node) bool { return !ok || (state == docker.StateRunning || state == docker.StateRestarting || state == docker.StatePaused) } +// IsStopped checks if the node is *not* a running docker container +var IsStopped = Complement(IsRunning) + +// FilterStopped filters out stopped containers. +func FilterStopped(r Renderer) Renderer { + return MakeFilter(IsStopped, r) +} + // FilterRunning filters out running containers. func FilterRunning(r Renderer) Renderer { - return MakeFilter(Complement(IsRunning), r) + return MakeFilter(IsRunning, r) } // FilterNonProcspied removes endpoints which were not found in procspy. func FilterNonProcspied(r Renderer) Renderer { - return MakeSilentFilter( + return MakeFilter( func(node report.Node) bool { _, ok := node.Latest.Lookup(endpoint.Procspied) return ok @@ -213,8 +209,8 @@ func FilterNonProcspied(r Renderer) Renderer { ) } -// IsSystem checks if the node is a "system" node -func IsSystem(n report.Node) bool { +// IsApplication checks if the node is an "application" node +func IsApplication(n report.Node) bool { containerName, _ := n.Latest.Lookup(docker.ContainerName) if _, ok := systemContainerNames[containerName]; ok { return false @@ -239,14 +235,40 @@ func IsSystem(n report.Node) bool { return true } +// IsSystem checks if the node is a "system" node +var IsSystem = Complement(IsApplication) + // FilterSystem is a Renderer which filters out system nodes. func FilterSystem(r Renderer) Renderer { return MakeFilter(IsSystem, r) } -// FilterApplication is a Renderer which filters out system nodes. +// FilterApplication is a Renderer which filters out application nodes. func FilterApplication(r Renderer) Renderer { - return MakeFilter(Complement(IsSystem), r) + return MakeFilter(IsApplication, r) +} + +// FilterEmpty is a Renderer which filters out nodes which have no children +// from the specified topology. +func FilterEmpty(topology string, r Renderer) Renderer { + return MakeFilter(HasChildren(topology), r) +} + +// HasChildren returns true if the node has no children from the specified +// topology. +func HasChildren(topology string) FilterFunc { + return func(n report.Node) bool { + if n.Topology == Pseudo { + return true + } + count := 0 + n.Children.ForEach(func(child report.Node) { + if child.Topology == topology { + count++ + } + }) + return count > 0 + } } var systemContainerNames = map[string]struct{}{ diff --git a/render/filters_test.go b/render/filters_test.go index 1e36a62246..15885025dd 100644 --- a/render/filters_test.go +++ b/render/filters_test.go @@ -10,15 +10,13 @@ import ( ) func TestFilterRender(t *testing.T) { - renderer := render.FilterUnconnected( - mockRenderer{Nodes: report.Nodes{ - "foo": report.MakeNode("foo").WithAdjacent("bar"), - "bar": report.MakeNode("bar").WithAdjacent("foo"), - "baz": report.MakeNode("baz"), - }}) - + renderer := mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode("foo").WithAdjacent("bar"), + "bar": report.MakeNode("bar").WithAdjacent("foo"), + "baz": report.MakeNode("baz"), + }} have := report.MakeIDList() - for id := range renderer.Render(report.MakeReport()) { + for id := range renderer.Render(report.MakeReport(), render.FilterUnconnected) { have = have.Add(id) } want := report.MakeIDList("foo", "bar") @@ -29,18 +27,21 @@ func TestFilterRender(t *testing.T) { func TestFilterRender2(t *testing.T) { // Test adjacencies are removed for filtered nodes. - renderer := render.Filter{ - FilterFunc: func(node report.Node) bool { - return node.ID != "bar" - }, - Renderer: mockRenderer{Nodes: report.Nodes{ - "foo": report.MakeNode("foo").WithAdjacent("bar"), - "bar": report.MakeNode("bar").WithAdjacent("foo"), - "baz": report.MakeNode("baz"), - }}, + filter := func(renderer render.Renderer) render.Renderer { + return &render.Filter{ + FilterFunc: func(node report.Node) bool { + return node.ID != "bar" + }, + Renderer: renderer, + } } + renderer := mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode("foo").WithAdjacent("bar"), + "bar": report.MakeNode("bar").WithAdjacent("foo"), + "baz": report.MakeNode("baz"), + }} - have := renderer.Render(report.MakeReport()) + have := renderer.Render(report.MakeReport(), filter) if have["foo"].Adjacency.Contains("bar") { t.Error("adjacencies for removed nodes should have been removed") } @@ -55,46 +56,55 @@ func TestFilterUnconnectedPseudoNodes(t *testing.T) { "bar": report.MakeNode("bar").WithAdjacent("baz"), "baz": report.MakeNode("baz").WithTopology(render.Pseudo), } - renderer := render.Filter{ - FilterFunc: func(node report.Node) bool { - return true - }, - Renderer: mockRenderer{Nodes: nodes}, + renderer := mockRenderer{Nodes: nodes} + filter := func(renderer render.Renderer) render.Renderer { + return &render.Filter{ + FilterFunc: func(node report.Node) bool { + return true + }, + Renderer: renderer, + } } want := nodes - have := renderer.Render(report.MakeReport()) + have := renderer.Render(report.MakeReport(), filter) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } } { - renderer := render.Filter{ - FilterFunc: func(node report.Node) bool { - return node.ID != "bar" - }, - Renderer: mockRenderer{Nodes: report.Nodes{ - "foo": report.MakeNode("foo").WithAdjacent("bar"), - "bar": report.MakeNode("bar").WithAdjacent("baz"), - "baz": report.MakeNode("baz").WithTopology(render.Pseudo), - }}, + filter := func(renderer render.Renderer) render.Renderer { + return &render.Filter{ + FilterFunc: func(node report.Node) bool { + return node.ID != "bar" + }, + Renderer: renderer, + } } - have := renderer.Render(report.MakeReport()) + renderer := mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode("foo").WithAdjacent("bar"), + "bar": report.MakeNode("bar").WithAdjacent("baz"), + "baz": report.MakeNode("baz").WithTopology(render.Pseudo), + }} + have := renderer.Render(report.MakeReport(), filter) if _, ok := have["baz"]; ok { t.Error("expected the unconnected pseudonode baz to have been removed") } } { - renderer := render.Filter{ - FilterFunc: func(node report.Node) bool { - return node.ID != "bar" - }, - Renderer: mockRenderer{Nodes: report.Nodes{ - "foo": report.MakeNode("foo"), - "bar": report.MakeNode("bar").WithAdjacent("foo"), - "baz": report.MakeNode("baz").WithTopology(render.Pseudo).WithAdjacent("bar"), - }}, + filter := func(renderer render.Renderer) render.Renderer { + return &render.Filter{ + FilterFunc: func(node report.Node) bool { + return node.ID != "bar" + }, + Renderer: renderer, + } } - have := renderer.Render(report.MakeReport()) + renderer := mockRenderer{Nodes: report.Nodes{ + "foo": report.MakeNode("foo"), + "bar": report.MakeNode("bar").WithAdjacent("foo"), + "baz": report.MakeNode("baz").WithTopology(render.Pseudo).WithAdjacent("bar"), + }} + have := renderer.Render(report.MakeReport(), filter) if _, ok := have["baz"]; ok { t.Error("expected the unconnected pseudonode baz to have been removed") } @@ -107,8 +117,8 @@ func TestFilterUnconnectedSelf(t *testing.T) { nodes := report.Nodes{ "foo": report.MakeNode("foo").WithAdjacent("foo"), } - renderer := render.FilterUnconnected(mockRenderer{Nodes: nodes}) - have := renderer.Render(report.MakeReport()) + renderer := mockRenderer{Nodes: nodes} + have := renderer.Render(report.MakeReport(), render.FilterUnconnected) if len(have) > 0 { t.Error("expected node only connected to self to be removed") } @@ -122,8 +132,8 @@ func TestFilterPseudo(t *testing.T) { "foo": report.MakeNode("foo"), "bar": report.MakeNode("bar").WithTopology(render.Pseudo), } - renderer := render.FilterPseudo(mockRenderer{Nodes: nodes}) - have := renderer.Render(report.MakeReport()) + renderer := mockRenderer{Nodes: nodes} + have := renderer.Render(report.MakeReport(), render.FilterPseudo) if _, ok := have["bar"]; ok { t.Error("expected pseudonode to be removed") } diff --git a/render/host_test.go b/render/host_test.go index cc39f6ad28..26c77f159d 100644 --- a/render/host_test.go +++ b/render/host_test.go @@ -11,8 +11,8 @@ import ( ) func TestHostRenderer(t *testing.T) { - have := render.HostRenderer.Render(fixture.Report).Prune() - want := expected.RenderedHosts.Prune() + have := Prune(render.HostRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedHosts) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/memoise.go b/render/memoise.go index 83371d50ca..08593a0c10 100644 --- a/render/memoise.go +++ b/render/memoise.go @@ -27,13 +27,17 @@ func Memoise(r Renderer) Renderer { // 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) report.Nodes { +func (m *memoise) Render(rpt report.Report, dct Decorator) report.Nodes { key := fmt.Sprintf("%s-%s", rpt.ID, m.id) - if result, err := renderCache.Get(key); err == nil { - return result.(report.Nodes) + if dct == nil { + if result, err := renderCache.Get(key); err == nil { + return result.(report.Nodes) + } + } + output := m.Renderer.Render(rpt, dct) + if dct == nil { + renderCache.Set(key, output) } - output := m.Renderer.Render(rpt) - renderCache.Set(key, output) return output } diff --git a/render/memoise_test.go b/render/memoise_test.go index e6eac26828..22347307b5 100644 --- a/render/memoise_test.go +++ b/render/memoise_test.go @@ -11,8 +11,8 @@ import ( type renderFunc func(r report.Report) report.Nodes -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 (f renderFunc) Render(r report.Report, _ render.Decorator) report.Nodes { return f(r) } +func (f renderFunc) Stats(r report.Report, _ render.Decorator) render.Stats { return render.Stats{} } func TestMemoise(t *testing.T) { calls := 0 @@ -23,7 +23,7 @@ func TestMemoise(t *testing.T) { m := render.Memoise(r) rpt1 := report.MakeReport() - result1 := m.Render(rpt1) + result1 := m.Render(rpt1, nil) // it should have rendered it. if _, ok := result1[rpt1.ID]; !ok { t.Errorf("Expected rendered report to contain a node, but got: %v", result1) @@ -32,7 +32,7 @@ func TestMemoise(t *testing.T) { t.Errorf("Expected renderer to have been called the first time") } - result2 := m.Render(rpt1) + result2 := m.Render(rpt1, nil) if !reflect.DeepEqual(result1, result2) { t.Errorf("Expected memoised result to be returned: %s", test.Diff(result1, result2)) } @@ -41,7 +41,7 @@ func TestMemoise(t *testing.T) { } rpt2 := report.MakeReport() - result3 := m.Render(rpt2) + result3 := m.Render(rpt2, nil) if reflect.DeepEqual(result1, result3) { t.Errorf("Expected different result for different report, but were the same") } @@ -50,7 +50,7 @@ func TestMemoise(t *testing.T) { } render.ResetCache() - result4 := m.Render(rpt1) + result4 := m.Render(rpt1, nil) if !reflect.DeepEqual(result1, result4) { t.Errorf("Expected original result to be returned: %s", test.Diff(result1, result4)) } diff --git a/render/pod.go b/render/pod.go index aabcc82145..623eea80e1 100644 --- a/render/pod.go +++ b/render/pod.go @@ -15,29 +15,33 @@ const ( // PodRenderer is a Renderer which produces a renderable kubernetes // graph by merging the container graph and the pods topology. -var PodRenderer = MakeReduce( - MakeSilentFilter( - func(n report.Node) bool { - // Drop unconnected pseudo nodes (could appear due to filtering) - _, isConnected := n.Latest.Lookup(IsConnected) - return n.Topology != Pseudo || isConnected - }, - ColorConnected(MakeMap( - MapContainer2Pod, - ContainerRenderer, - )), +var PodRenderer = FilterEmpty(report.Container, + MakeReduce( + MakeFilter( + func(n report.Node) bool { + // Drop unconnected pseudo nodes (could appear due to filtering) + _, isConnected := n.Latest.Lookup(IsConnected) + return n.Topology != Pseudo || isConnected + }, + ColorConnected(MakeMap( + MapContainer2Pod, + ContainerWithImageNameRenderer, + )), + ), + SelectPod, ), - SelectPod, ) // PodServiceRenderer is a Renderer which produces a renderable kubernetes services // graph by merging the pods graph and the services topology. -var PodServiceRenderer = MakeReduce( - MakeMap( - MapPod2Service, - PodRenderer, +var PodServiceRenderer = FilterEmpty(report.Pod, + MakeReduce( + MakeMap( + MapPod2Service, + PodRenderer, + ), + SelectService, ), - SelectService, ) // MapContainer2Pod maps container Nodes to pod diff --git a/render/pod_test.go b/render/pod_test.go index d4cf06c041..d4776939e3 100644 --- a/render/pod_test.go +++ b/render/pod_test.go @@ -7,14 +7,15 @@ import ( "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/render" "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 TestPodRenderer(t *testing.T) { - have := render.PodRenderer.Render(fixture.Report).Prune() - want := expected.RenderedPods.Prune() + have := Prune(render.PodRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedPods) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -32,8 +33,8 @@ func TestPodFilterRenderer(t *testing.T) { input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{ docker.LabelPrefix + "io.kubernetes.pod.name": "kube-system/foo", }) - have := render.FilterSystem(render.PodRenderer).Render(input).Prune() - want := expected.RenderedPods.Copy().Prune() + have := Prune(render.PodRenderer.Render(input, render.FilterApplication)) + want := Prune(expected.RenderedPods.Copy()) delete(want, fixture.ClientPodNodeID) delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { @@ -42,8 +43,36 @@ func TestPodFilterRenderer(t *testing.T) { } func TestPodServiceRenderer(t *testing.T) { - have := render.PodServiceRenderer.Render(fixture.Report).Prune() - want := expected.RenderedPodServices.Prune() + have := Prune(render.PodServiceRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedPodServices) + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + +func TestPodServiceFilterRenderer(t *testing.T) { + // tag on containers or pod namespace in the topology and ensure + // it is filtered out correctly. + input := fixture.Report.Copy() + input.Pod.Nodes[fixture.ClientPodNodeID] = input.Pod.Nodes[fixture.ClientPodNodeID].WithLatests(map[string]string{ + kubernetes.PodID: "pod:kube-system/foo", + kubernetes.Namespace: "kube-system", + kubernetes.PodName: "foo", + }) + input.Container.Nodes[fixture.ClientContainerNodeID] = input.Container.Nodes[fixture.ClientContainerNodeID].WithLatests(map[string]string{ + docker.LabelPrefix + "io.kubernetes.pod.name": "kube-system/foo", + }) + have := Prune(render.PodServiceRenderer.Render(input, render.FilterApplication)) + want := Prune(expected.RenderedPodServices.Copy()) + wantNode := want[fixture.ServiceNodeID] + wantNode.Adjacency = nil + wantNode.Children = report.MakeNodeSet( + expected.RenderedEndpoints[fixture.Server80NodeID], + expected.RenderedProcesses[fixture.ServerProcessNodeID], + expected.RenderedContainers[fixture.ServerContainerNodeID], + expected.RenderedPods[fixture.ServerPodNodeID], + ) + want[fixture.ServiceNodeID] = wantNode if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/process.go b/render/process.go index f1471442ce..92d55c3e2c 100644 --- a/render/process.go +++ b/render/process.go @@ -42,9 +42,9 @@ type processWithContainerNameRenderer struct { Renderer } -func (r processWithContainerNameRenderer) Render(rpt report.Report) report.Nodes { - processes := r.Renderer.Render(rpt) - containers := SelectContainer.Render(rpt) +func (r processWithContainerNameRenderer) Render(rpt report.Report, dct Decorator) report.Nodes { + processes := r.Renderer.Render(rpt, dct) + containers := SelectContainer.Render(rpt, dct) outputs := report.Nodes{} for id, p := range processes { diff --git a/render/process_test.go b/render/process_test.go index 54d649b955..fd5b75481a 100644 --- a/render/process_test.go +++ b/render/process_test.go @@ -11,24 +11,24 @@ import ( ) func TestEndpointRenderer(t *testing.T) { - have := render.EndpointRenderer.Render(fixture.Report).Prune() - want := expected.RenderedEndpoints.Prune() + have := Prune(render.EndpointRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedEndpoints) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } } func TestProcessRenderer(t *testing.T) { - have := render.ProcessRenderer.Render(fixture.Report).Prune() - want := expected.RenderedProcesses.Prune() + have := Prune(render.ProcessRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedProcesses) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } } func TestProcessNameRenderer(t *testing.T) { - have := render.ProcessNameRenderer.Render(fixture.Report).Prune() - want := expected.RenderedProcessNames.Prune() + have := Prune(render.ProcessNameRenderer.Render(fixture.Report, render.FilterNoop)) + want := Prune(expected.RenderedProcessNames) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/render.go b/render/render.go index 7625169fa1..13d38c1470 100644 --- a/render/render.go +++ b/render/render.go @@ -12,8 +12,8 @@ type MapFunc func(report.Node, report.Networks) report.Nodes // Renderer is something that can render a report to a set of Nodes. type Renderer interface { - Render(report.Report) report.Nodes - Stats(report.Report) Stats + Render(report.Report, Decorator) report.Nodes + Stats(report.Report, Decorator) Stats } // Stats is the type returned by Renderer.Stats @@ -38,19 +38,19 @@ func MakeReduce(renderers ...Renderer) Renderer { } // Render produces a set of Nodes given a Report. -func (r *Reduce) Render(rpt report.Report) report.Nodes { +func (r *Reduce) Render(rpt report.Report, dct Decorator) report.Nodes { result := report.Nodes{} for _, renderer := range *r { - result = result.Merge(renderer.Render(rpt)) + result = result.Merge(renderer.Render(rpt, dct)) } return result } // Stats implements Renderer -func (r *Reduce) Stats(rpt report.Report) Stats { +func (r *Reduce) Stats(rpt report.Report, dct Decorator) Stats { var result Stats for _, renderer := range *r { - result = result.merge(renderer.Stats(rpt)) + result = result.merge(renderer.Stats(rpt, dct)) } return result } @@ -69,9 +69,9 @@ func MakeMap(f MapFunc, r Renderer) Renderer { // Render transforms a set of Nodes produces by another Renderer. // using a map function -func (m *Map) Render(rpt report.Report) report.Nodes { +func (m *Map) Render(rpt report.Report, dct Decorator) report.Nodes { var ( - input = m.Renderer.Render(rpt) + input = m.Renderer.Render(rpt, dct) 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 @@ -109,9 +109,45 @@ func (m *Map) Render(rpt report.Report) report.Nodes { } // Stats implements Renderer -func (m *Map) Stats(rpt report.Report) Stats { +func (m *Map) Stats(_ report.Report, _ Decorator) 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{} } + +// Decorator transforms one renderer to another. e.g. Filters. +type Decorator func(Renderer) Renderer + +// ComposeDecorators composes decorators into one. +func ComposeDecorators(decorators ...Decorator) Decorator { + return func(r Renderer) Renderer { + for _, decorator := range decorators { + r = decorator(r) + } + return r + } +} + +type applyDecorator struct { + Renderer +} + +func (ad applyDecorator) Render(rpt report.Report, dct Decorator) report.Nodes { + if dct != nil { + return dct(ad.Renderer).Render(rpt, nil) + } + return ad.Renderer.Render(rpt, nil) +} +func (ad applyDecorator) Stats(rpt report.Report, dct Decorator) Stats { + if dct != nil { + return dct(ad.Renderer).Stats(rpt, nil) + } + return ad.Renderer.Stats(rpt, nil) +} + +// ApplyDecorators returns a renderer which will apply the given decorators +// to the child render. +func ApplyDecorators(renderer Renderer) Renderer { + return applyDecorator{renderer} +} diff --git a/render/render_test.go b/render/render_test.go index f802d74e2d..3763299e3f 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -13,8 +13,38 @@ type mockRenderer struct { report.Nodes } -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 (m mockRenderer) Render(rpt report.Report, d render.Decorator) report.Nodes { + if d != nil { + return d(mockRenderer{m.Nodes}).Render(rpt, nil) + } + return m.Nodes +} +func (m mockRenderer) Stats(rpt report.Report, _ render.Decorator) render.Stats { return render.Stats{} } + +// Prune returns a copy of the Nodes with all information not strictly +// necessary for rendering nodes and edges in the UI cut away. +func Prune(nodes report.Nodes) report.Nodes { + result := report.Nodes{} + for id, node := range nodes { + result[id] = PruneNode(node) + } + return result +} + +// PruneNode 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 PruneNode(node report.Node) report.Node { + prunedChildren := report.MakeNodeSet() + node.Children.ForEach(func(child report.Node) { + prunedChildren = prunedChildren.Add(PruneNode(child)) + }) + return report.MakeNode( + node.ID). + WithTopology(node.Topology). + WithAdjacent(node.Adjacency.Copy()...). + WithChildren(prunedChildren) +} func TestReduceRender(t *testing.T) { renderer := render.Reduce([]render.Renderer{ @@ -26,7 +56,7 @@ func TestReduceRender(t *testing.T) { "foo": report.MakeNode("foo"), "bar": report.MakeNode("bar"), } - have := renderer.Render(report.MakeReport()) + have := renderer.Render(report.MakeReport(), render.FilterNoop) if !reflect.DeepEqual(want, have) { t.Errorf("want %+v, have %+v", want, have) } @@ -43,7 +73,7 @@ func TestMapRender1(t *testing.T) { }}, } want := report.Nodes{} - have := mapper.Render(report.MakeReport()) + have := mapper.Render(report.MakeReport(), render.FilterNoop) if !reflect.DeepEqual(want, have) { t.Errorf("want %+v, have %+v", want, have) } @@ -65,7 +95,7 @@ func TestMapRender2(t *testing.T) { want := report.Nodes{ "bar": report.MakeNode("bar"), } - have := mapper.Render(report.MakeReport()) + have := mapper.Render(report.MakeReport(), render.FilterNoop) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -87,7 +117,7 @@ func TestMapRender3(t *testing.T) { "_foo": report.MakeNode("_foo").WithAdjacent("_baz"), "_baz": report.MakeNode("_baz").WithAdjacent("_foo"), } - have := mapper.Render(report.MakeReport()) + have := mapper.Render(report.MakeReport(), render.FilterNoop) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/selectors.go b/render/selectors.go index 683382872a..b1bba8213a 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -9,13 +9,13 @@ import ( type TopologySelector string // Render implements Renderer -func (t TopologySelector) Render(r report.Report) report.Nodes { +func (t TopologySelector) Render(r report.Report, _ Decorator) report.Nodes { topology, _ := r.Topology(string(t)) return topology.Nodes } // Stats implements Renderer -func (t TopologySelector) Stats(r report.Report) Stats { +func (t TopologySelector) Stats(r report.Report, _ Decorator) Stats { return Stats{} } diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go index 99f1b60d08..cf2b133c54 100644 --- a/render/short_lived_connections_test.go +++ b/render/short_lived_connections_test.go @@ -73,7 +73,7 @@ var ( ) func TestShortLivedInternetNodeConnections(t *testing.T) { - have := render.ContainerWithImageNameRenderer.Render(rpt).Prune() + have := Prune(render.ContainerWithImageNameRenderer.Render(rpt, render.FilterNoop)) // Conntracked-only connections from the internet should be assigned to the internet pseudonode internet, ok := have[render.IncomingInternetID] diff --git a/report/node.go b/report/node.go index 868deb34f6..f7ff0ff682 100644 --- a/report/node.go +++ b/report/node.go @@ -214,18 +214,3 @@ func (n Node) Merge(other Node) Node { 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( - n.ID). - WithTopology(n.Topology). - WithAdjacent(n.Adjacency.Copy()...). - WithChildren(prunedChildren) -} diff --git a/report/node_set.go b/report/node_set.go index 4b278d1bf1..695dd6fc4f 100644 --- a/report/node_set.go +++ b/report/node_set.go @@ -40,6 +40,22 @@ func (n NodeSet) Add(nodes ...Node) NodeSet { return NodeSet{result} } +// Delete deletes the nodes from the NodeSet by ID. Delete is the only valid +// way to shrink a NodeSet. Delete returns the NodeSet to enable chaining. +func (n NodeSet) Delete(ids ...string) NodeSet { + if n.Size() == 0 { + return n + } + result := n.psMap + for _, id := range ids { + result = result.Delete(id) + } + if result.Size() == 0 { + return EmptyNodeSet + } + return NodeSet{result} +} + // Merge combines the two NodeSets and returns a new result. func (n NodeSet) Merge(other NodeSet) NodeSet { nSize, otherSize := n.Size(), other.Size() diff --git a/report/node_set_test.go b/report/node_set_test.go index 45f56d4559..0ffbfee448 100644 --- a/report/node_set_test.go +++ b/report/node_set_test.go @@ -167,6 +167,63 @@ func BenchmarkNodeSetAdd(b *testing.B) { } } +func TestNodeSetDelete(t *testing.T) { + for _, testcase := range []struct { + input report.NodeSet + nodes []string + want report.NodeSet + }{ + { + input: report.NodeSet{}, + nodes: []string{}, + want: report.NodeSet{}, + }, + { + input: report.EmptyNodeSet, + nodes: []string{}, + want: report.EmptyNodeSet, + }, + { + input: report.MakeNodeSet(report.MakeNode("a")), + nodes: []string{}, + want: report.MakeNodeSet(report.MakeNode("a")), + }, + { + input: report.EmptyNodeSet, + nodes: []string{"a"}, + want: report.EmptyNodeSet, + }, + { + input: report.MakeNodeSet(report.MakeNode("a")), + nodes: []string{"a"}, + want: report.EmptyNodeSet, + }, + { + input: report.MakeNodeSet(report.MakeNode("b")), + nodes: []string{"a", "b"}, + want: report.EmptyNodeSet, + }, + { + input: report.MakeNodeSet(report.MakeNode("a")), + nodes: []string{"c", "b"}, + want: report.MakeNodeSet(report.MakeNode("a")), + }, + { + input: report.MakeNodeSet(report.MakeNode("a"), report.MakeNode("c")), + nodes: []string{"a", "a", "a"}, + want: report.MakeNodeSet(report.MakeNode("c")), + }, + } { + originalLen := testcase.input.Size() + if want, have := testcase.want, testcase.input.Delete(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 TestNodeSetMerge(t *testing.T) { for _, testcase := range []struct { input report.NodeSet diff --git a/report/topology.go b/report/topology.go index 989cdfbd23..da57e994b8 100644 --- a/report/topology.go +++ b/report/topology.go @@ -185,16 +185,6 @@ 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 7427e465cd..f6dc2f9beb 100644 --- a/test/fixture/report_fixture.go +++ b/test/fixture/report_fixture.go @@ -75,9 +75,13 @@ var ( ClientContainerID = "a1b2c3d4e5" ClientContainerName = "client" ServerContainerID = "5e4d3c2b1a" + ServerContainerName = "task-name-5-server-aceb93e2f2b797caba01" ClientContainerNodeID = report.MakeContainerNodeID(ClientContainerID) ServerContainerNodeID = report.MakeContainerNodeID(ServerContainerID) + ClientContainerHostname = ClientContainerName + ".hostname.com" + ServerContainerHostname = ServerContainerName + ".hostname.com" + ClientContainerImageID = "imageid123" ServerContainerImageID = "imageid456" ClientContainerImageNodeID = report.MakeContainerImageNodeID(ClientContainerImageID) @@ -256,6 +260,7 @@ var ( ClientContainerNodeID, map[string]string{ docker.ContainerID: ClientContainerID, docker.ContainerName: ClientContainerName, + docker.ContainerHostname: ClientContainerHostname, docker.ImageID: ClientContainerImageID, report.HostNodeID: ClientHostNodeID, docker.LabelPrefix + "io.kubernetes.pod.name": ClientPodID, @@ -276,7 +281,8 @@ var ( ServerContainerNodeID, map[string]string{ docker.ContainerID: ServerContainerID, - docker.ContainerName: "task-name-5-server-aceb93e2f2b797caba01", + docker.ContainerName: ServerContainerName, + docker.ContainerHostname: ServerContainerHostname, docker.ContainerState: docker.StateRunning, docker.ContainerStateHuman: docker.StateRunning, docker.ImageID: ServerContainerImageID,