Merge pull request #1371 from weaveworks/1219-grouped-node-counts-2

Fixing grouped node count for filtered children nodes
This commit is contained in:
Paul Bellamy
2016-04-28 13:30:15 +01:00
29 changed files with 614 additions and 293 deletions
+34 -39
View File
@@ -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)
}
}
+20 -7
View File
@@ -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
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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
}
+19 -10
View File
@@ -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)
}
}
+39 -18
View File
@@ -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)}
}
+69 -7
View File
@@ -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))
}
+5 -5
View File
@@ -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{
+5 -5
View File
@@ -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"},
+2 -2
View File
@@ -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 {
+41 -9
View File
@@ -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{
+74 -52
View File
@@ -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
@@ -243,14 +239,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{}{
+58 -48
View File
@@ -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")
}
+2 -2
View File
@@ -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))
}
+9 -5
View File
@@ -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
}
+6 -6
View File
@@ -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))
}
+21 -17
View File
@@ -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
+35 -6
View File
@@ -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))
}
+3 -3
View File
@@ -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 {
+6 -6
View File
@@ -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))
}
+45 -9
View File
@@ -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}
}
+36 -6
View File
@@ -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))
}
+2 -2
View File
@@ -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{}
}
+1 -1
View File
@@ -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]
-15
View File
@@ -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)
}
+16
View File
@@ -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()
+57
View File
@@ -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
-10
View File
@@ -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{}
+7 -1
View File
@@ -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,