From e7ebb83e2389ffda30f2c9f4f68de41587d2d5c3 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Thu, 21 Apr 2016 14:48:33 +0100 Subject: [PATCH 1/3] Prune is only used in tests, so let's move it there --- render/container_test.go | 12 ++++++------ render/host_test.go | 4 ++-- render/pod_test.go | 12 ++++++------ render/process_test.go | 12 ++++++------ render/render_test.go | 25 +++++++++++++++++++++++++ render/short_lived_connections_test.go | 2 +- report/node.go | 15 --------------- report/topology.go | 10 ---------- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/render/container_test.go b/render/container_test.go index 5c2fde89d..bfdbb827f 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.ContainerRenderer.Render(fixture.Report)) + 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.FilterSystem(render.ContainerRenderer).Render(input)) + want := Prune(expected.RenderedContainers.Copy()) delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -92,8 +92,8 @@ func TestContainerWithHostIPsRenderer(t *testing.T) { } func TestContainerImageRenderer(t *testing.T) { - have := render.ContainerImageRenderer.Render(fixture.Report).Prune() - want := expected.RenderedContainerImages.Prune() + have := Prune(render.ContainerImageRenderer.Render(fixture.Report)) + want := Prune(expected.RenderedContainerImages) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/host_test.go b/render/host_test.go index cc39f6ad2..9a1731779 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)) + want := Prune(expected.RenderedHosts) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/pod_test.go b/render/pod_test.go index d4cf06c04..3d066842b 100644 --- a/render/pod_test.go +++ b/render/pod_test.go @@ -13,8 +13,8 @@ import ( ) func TestPodRenderer(t *testing.T) { - have := render.PodRenderer.Render(fixture.Report).Prune() - want := expected.RenderedPods.Prune() + have := Prune(render.PodRenderer.Render(fixture.Report)) + want := Prune(expected.RenderedPods) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } @@ -32,8 +32,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.FilterSystem(render.PodRenderer).Render(input)) + want := Prune(expected.RenderedPods.Copy()) delete(want, fixture.ClientPodNodeID) delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(want, have) { @@ -42,8 +42,8 @@ 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)) + want := Prune(expected.RenderedPodServices) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/process_test.go b/render/process_test.go index 54d649b95..abb8a8ac3 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)) + 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)) + 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)) + want := Prune(expected.RenderedProcessNames) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) } diff --git a/render/render_test.go b/render/render_test.go index f802d74e2..079408c51 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -16,6 +16,31 @@ type mockRenderer struct { func (m mockRenderer) Render(rpt report.Report) report.Nodes { return m.Nodes } func (m mockRenderer) Stats(rpt report.Report) 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{ mockRenderer{Nodes: report.Nodes{"foo": report.MakeNode("foo")}}, diff --git a/render/short_lived_connections_test.go b/render/short_lived_connections_test.go index 99f1b60d0..ee009c865 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)) // 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 868deb34f..f7ff0ff68 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/topology.go b/report/topology.go index 989cdfbd2..da57e994b 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{} From fdaafd22088fcb2b9c037644c411870081c21152 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Thu, 21 Apr 2016 17:22:19 +0100 Subject: [PATCH 2/3] Check counters on expected rendered topologies --- render/container_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/render/container_test.go b/render/container_test.go index bfdbb827f..14029e24f 100644 --- a/render/container_test.go +++ b/render/container_test.go @@ -98,3 +98,33 @@ func TestContainerImageRenderer(t *testing.T) { 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() + + // TODO: Add a process and endpoint here to make this test fail, so we can fix it. + + clientContainer2ID = "f6g7h8i9j1" + clientContainer2NodeID = report.MakeContainerNodeID(fixture.ClientContainerID) + 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.FilterSystem(render.ContainerImageRenderer).Render(input)) + 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)) + } +} From 3d3aed2bb31f783427fcb7a991dd49faf3a04f4e Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Thu, 21 Apr 2016 17:48:56 +0100 Subject: [PATCH 3/3] Fixing grouped node count for filtered children nodes Squash of: * We have to keep all the container hostnames until the end so we can count how many we've filtered * Adding tests for ContainerHostnameRenderer and PodServiceRenderer with filters * Because we filter on image name we need the image name before filtering * Alternative approach to passing decorators. * Refactor out some of the decorator capture * Don't memoise decorated calls to Render * Fixing filtered counts on containers topology Tricky, because we need the filters to be silent sometimes (when they're in the middle), but not when they're at the top, so we take the "top" filter's stats. However, this means we have to compose all user-specified filters into a single Filter layer, so we can get all stats. There are no more Silent filters, as all filters are silent (unless they are at the top). Additionally, I clarified some of the filters as their usage/terminology was inconsistent and confused. Now Filter(IsFoo, ...) *keeps* only nodes where IsFoo is true. --- app/api_topologies.go | 73 +++++++------- app/api_topology.go | 27 ++++-- app/router.go | 2 +- experimental/graphviz/render.go | 2 +- render/benchmark_test.go | 29 ++++-- render/container.go | 57 +++++++---- render/container_test.go | 50 ++++++++-- render/detailed/node_test.go | 10 +- render/detailed/parents_test.go | 10 +- render/detailed/summary_test.go | 4 +- render/expected/expected.go | 50 ++++++++-- render/filters.go | 126 +++++++++++++++---------- render/filters_test.go | 106 +++++++++++---------- render/host_test.go | 2 +- render/memoise.go | 14 ++- render/memoise_test.go | 12 +-- render/pod.go | 38 ++++---- render/pod_test.go | 35 ++++++- render/process.go | 6 +- render/process_test.go | 6 +- render/render.go | 54 +++++++++-- render/render_test.go | 17 ++-- render/selectors.go | 4 +- render/short_lived_connections_test.go | 2 +- report/node_set.go | 16 ++++ report/node_set_test.go | 57 +++++++++++ test/fixture/report_fixture.go | 8 +- 27 files changed, 554 insertions(+), 263 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index e152dc12a..9c400e34e 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 57796d90f..d9ecd7378 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 3f5982c8a..d2ca136dd 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 511184b6f..5433db826 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 05889b21e..ecf00d328 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 c15f8762e..55fc9fc81 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 14029e24f..75a60ae59 100644 --- a/render/container_test.go +++ b/render/container_test.go @@ -47,7 +47,7 @@ func testMap(t *testing.T, f render.MapFunc, input testcase) { } func TestContainerRenderer(t *testing.T) { - have := Prune(render.ContainerRenderer.Render(fixture.Report)) + 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,7 +61,7 @@ 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 := Prune(render.FilterSystem(render.ContainerRenderer).Render(input)) + have := Prune(render.ContainerWithImageNameRenderer.Render(input, render.FilterApplication)) want := Prune(expected.RenderedContainers.Copy()) delete(want, fixture.ClientContainerNodeID) if !reflect.DeepEqual(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,8 +91,42 @@ 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 := Prune(render.ContainerImageRenderer.Render(fixture.Report)) + have := Prune(render.ContainerImageRenderer.Render(fixture.Report, render.FilterNoop)) want := Prune(expected.RenderedContainerImages) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -104,10 +138,8 @@ func TestContainerImageFilterRenderer(t *testing.T) { // it is filtered out correctly. input := fixture.Report.Copy() - // TODO: Add a process and endpoint here to make this test fail, so we can fix it. - - clientContainer2ID = "f6g7h8i9j1" - clientContainer2NodeID = report.MakeContainerNodeID(fixture.ClientContainerID) + clientContainer2ID := "f6g7h8i9j1" + clientContainer2NodeID := report.MakeContainerNodeID(clientContainer2ID) input.Container.AddNode(report.MakeNodeWith(clientContainer2NodeID, map[string]string{ docker.LabelPrefix + "works.weave.role": "system", @@ -119,7 +151,7 @@ func TestContainerImageFilterRenderer(t *testing.T) { Add("host", report.MakeStringSet(fixture.ClientHostNodeID)), ).WithTopology(report.ContainerImage)) - have := Prune(render.FilterSystem(render.ContainerImageRenderer).Render(input)) + 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 diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index 78c0e1a4a..17b6b7d09 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 7a21b18ba..5351acdb6 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 93e544e67..cfe1021ec 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 88a69fc30..351f19c99 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 61839292f..9dc4ab735 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 { +func MakeFilter(f FilterFunc, 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 { - 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 1e36a6224..15885025d 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 9a1731779..26c77f159 100644 --- a/render/host_test.go +++ b/render/host_test.go @@ -11,7 +11,7 @@ import ( ) func TestHostRenderer(t *testing.T) { - have := Prune(render.HostRenderer.Render(fixture.Report)) + 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 83371d50c..08593a0c1 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 e6eac2682..22347307b 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 aabcc8214..623eea80e 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 3d066842b..d4776939e 100644 --- a/render/pod_test.go +++ b/render/pod_test.go @@ -7,13 +7,14 @@ 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 := Prune(render.PodRenderer.Render(fixture.Report)) + 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,7 +33,7 @@ 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 := Prune(render.FilterSystem(render.PodRenderer).Render(input)) + have := Prune(render.PodRenderer.Render(input, render.FilterApplication)) want := Prune(expected.RenderedPods.Copy()) delete(want, fixture.ClientPodNodeID) delete(want, fixture.ClientContainerNodeID) @@ -42,9 +43,37 @@ func TestPodFilterRenderer(t *testing.T) { } func TestPodServiceRenderer(t *testing.T) { - have := Prune(render.PodServiceRenderer.Render(fixture.Report)) + 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 f1471442c..92d55c3e2 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 abb8a8ac3..fd5b75481 100644 --- a/render/process_test.go +++ b/render/process_test.go @@ -11,7 +11,7 @@ import ( ) func TestEndpointRenderer(t *testing.T) { - have := Prune(render.EndpointRenderer.Render(fixture.Report)) + have := Prune(render.EndpointRenderer.Render(fixture.Report, render.FilterNoop)) want := Prune(expected.RenderedEndpoints) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -19,7 +19,7 @@ func TestEndpointRenderer(t *testing.T) { } func TestProcessRenderer(t *testing.T) { - have := Prune(render.ProcessRenderer.Render(fixture.Report)) + have := Prune(render.ProcessRenderer.Render(fixture.Report, render.FilterNoop)) want := Prune(expected.RenderedProcesses) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -27,7 +27,7 @@ func TestProcessRenderer(t *testing.T) { } func TestProcessNameRenderer(t *testing.T) { - have := Prune(render.ProcessNameRenderer.Render(fixture.Report)) + 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 7625169fa..13d38c147 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 079408c51..3763299e3 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -13,8 +13,13 @@ 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. @@ -51,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) } @@ -68,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) } @@ -90,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)) } @@ -112,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 683382872..b1bba8213 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 ee009c865..cf2b133c5 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 := Prune(render.ContainerWithImageNameRenderer.Render(rpt)) + 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_set.go b/report/node_set.go index 4b278d1bf..695dd6fc4 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 45f56d455..0ffbfee44 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/test/fixture/report_fixture.go b/test/fixture/report_fixture.go index 7427e465c..f6dc2f9be 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,