Merge pull request #420 from weaveworks/without-system-containers

Add filtering options to views; add options for filtering out system containers & unconnected containers.
This commit is contained in:
Peter Bourgon
2015-09-07 16:51:55 +02:00
16 changed files with 458 additions and 93 deletions
+37 -8
View File
@@ -9,10 +9,18 @@ import (
// APITopologyDesc is returned in a list by the /api/topology handler.
type APITopologyDesc struct {
Name string `json:"name"`
URL string `json:"url"`
SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"`
Stats *topologyStats `json:"stats,omitempty"`
Name string `json:"name"`
URL string `json:"url"`
SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"`
Options map[string][]APITopologyOption `json:"options"`
Stats *topologyStats `json:"stats,omitempty"`
}
// APITopologyOption describes a &param=value to a given topology.
type APITopologyOption struct {
Value string `json:"value"`
Display string `json:"display"`
Default bool `json:"default,omitempty"`
}
type topologyStats struct {
@@ -29,23 +37,30 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req
topologies = []APITopologyDesc{}
)
for name, def := range topologyRegistry {
// Don't show sub-topologies at the top level.
if def.parent != "" {
continue // subtopology, don't show at top level
continue
}
// Collect all sub-topologies of this one, depth=1 only.
subTopologies := []APITopologyDesc{}
for subName, subDef := range topologyRegistry {
if subDef.parent == name {
subTopologies = append(subTopologies, APITopologyDesc{
Name: subDef.human,
URL: "/api/topology/" + subName,
Stats: stats(subDef.renderer.Render(rpt)),
Name: subDef.human,
URL: "/api/topology/" + subName,
Options: makeTopologyOptions(subDef),
Stats: stats(subDef.renderer.Render(rpt)),
})
}
}
// Append.
topologies = append(topologies, APITopologyDesc{
Name: def.human,
URL: "/api/topology/" + name,
SubTopologies: subTopologies,
Options: makeTopologyOptions(def),
Stats: stats(def.renderer.Render(rpt)),
})
}
@@ -53,6 +68,20 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req
}
}
func makeTopologyOptions(view topologyView) map[string][]APITopologyOption {
options := map[string][]APITopologyOption{}
for param, optionVals := range view.options {
for _, optionVal := range optionVals {
options[param] = append(options[param], APITopologyOption{
Value: optionVal.value,
Display: optionVal.human,
Default: optionVal.def,
})
}
}
return options
}
func stats(r render.RenderableNodes) *topologyStats {
var (
nodes int
+28
View File
@@ -110,6 +110,14 @@ func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http
http.NotFound(w, r)
return
}
for param, opts := range topology.options {
value := r.FormValue(param)
for _, opt := range opts {
if (value == "" && opt.def) || (opt.value != "" && opt.value == value) {
topology.renderer = opt.decorator(topology.renderer)
}
}
}
f(rep, topology, w, r)
}
}
@@ -138,11 +146,19 @@ var topologyRegistry = map[string]topologyView{
human: "Containers",
parent: "",
renderer: render.ContainerWithImageNameRenderer{},
options: optionParams{"system": {
{"show", "Show system containers", false, nop},
{"hide", "Hide system containers", true, render.FilterSystem},
}},
},
"containers-by-image": {
human: "by image",
parent: "containers",
renderer: render.ContainerImageRenderer,
options: optionParams{"system": {
{"show", "Show system containers", false, nop},
{"hide", "Hide system containers", true, render.FilterSystem},
}},
},
"hosts": {
human: "Hosts",
@@ -155,4 +171,16 @@ type topologyView struct {
human string
parent string
renderer render.Renderer
options optionParams
}
type optionParams map[string][]optionValue // param: values
type optionValue struct {
value string // "hide"
human string // "Hide system containers"
def bool
decorator func(render.Renderer) render.Renderer
}
func nop(r render.Renderer) render.Renderer { return r }
+13 -3
View File
@@ -5,6 +5,16 @@ let RouterUtils;
let WebapiUtils;
module.exports = {
changeTopologyOption: function(option, value) {
AppDispatcher.dispatch({
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
option: option,
value: value
});
RouterUtils.updateRoute();
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
},
clickCloseDetails: function() {
AppDispatcher.dispatch({
type: ActionTypes.CLICK_CLOSE_DETAILS
@@ -27,7 +37,7 @@ module.exports = {
topologyId: topologyId
});
RouterUtils.updateRoute();
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
},
openWebsocket: function() {
@@ -96,7 +106,7 @@ module.exports = {
type: ActionTypes.RECEIVE_TOPOLOGIES,
topologies: topologies
});
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
},
@@ -119,7 +129,7 @@ module.exports = {
state: state,
type: ActionTypes.ROUTE_TOPOLOGY
});
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl());
WebapiUtils.getNodesDelta(AppStore.getCurrentTopologyUrl(), AppStore.getActiveTopologyOptions());
WebapiUtils.getNodeDetails(AppStore.getCurrentTopologyUrl(), AppStore.getSelectedNodeId());
}
};
+5 -1
View File
@@ -211,8 +211,12 @@ const NodesChart = React.createClass({
debug('graph layout took ' + timedLayouter.time + 'ms');
// adjust layout based on viewport
// layout was aborted
if (!graph) {
return;
}
// adjust layout based on viewport
const xFactor = (props.width - MARGINS.left - MARGINS.right) / graph.width;
const yFactor = props.height / graph.height;
const zoomFactor = Math.min(xFactor, yFactor);
+5
View File
@@ -4,6 +4,7 @@ const Logo = require('./logo');
const AppStore = require('../stores/app-store');
const Status = require('./status.js');
const Topologies = require('./topologies.js');
const TopologyOptions = require('./topology-options.js');
const WebapiUtils = require('../utils/web-api-utils');
const AppActions = require('../actions/app-actions');
const Details = require('./details');
@@ -14,8 +15,10 @@ const ESC_KEY_CODE = 27;
function getStateFromStores() {
return {
activeTopologyOptions: AppStore.getActiveTopologyOptions(),
currentTopology: AppStore.getCurrentTopology(),
currentTopologyId: AppStore.getCurrentTopologyId(),
currentTopologyOptions: AppStore.getCurrentTopologyOptions(),
errorUrl: AppStore.getErrorUrl(),
highlightedEdgeIds: AppStore.getHighlightedEdgeIds(),
highlightedNodeIds: AppStore.getHighlightedNodeIds(),
@@ -67,6 +70,8 @@ const App = React.createClass({
<div className="header">
<Logo />
<Topologies topologies={this.state.topologies} currentTopology={this.state.currentTopology} />
<TopologyOptions options={this.state.currentTopologyOptions}
activeOptions={this.state.activeTopologyOptions} />
<Status errorUrl={this.state.errorUrl} websocketClosed={this.state.websocketClosed} />
</div>
@@ -0,0 +1,77 @@
const React = require('react');
const _ = require('lodash');
const mui = require('material-ui');
const DropDownMenu = mui.DropDownMenu;
const AppActions = require('../actions/app-actions');
const TopologyOptions = React.createClass({
componentDidMount: function() {
this.fixWidth();
},
onChange: function(ev, index, item) {
ev.preventDefault();
AppActions.changeTopologyOption(item.option, item.payload);
},
renderOption: function(items) {
let selected = 0;
let key;
const activeOptions = this.props.activeOptions;
const menuItems = items.map(function(item, index) {
if (activeOptions[item.option] && activeOptions[item.option] === item.value) {
selected = index;
}
key = item.option;
return {
option: item.option,
payload: item.value,
text: item.display
};
});
return (
<DropDownMenu menuItems={menuItems} onChange={this.onChange} key={key}
selectedIndex={selected} />
);
},
render: function() {
const options = _.sortBy(
_.map(this.props.options, function(items, optionId) {
_.each(items, function(item) {
item.option = optionId;
});
items.option = optionId;
return items;
}),
'option'
);
return (
<div className="topology-options" ref="container">
{options.map(function(items) {
return this.renderOption(items);
}, this)}
</div>
);
},
componentDidUpdate: function() {
this.fixWidth();
},
fixWidth: function() {
const containerNode = this.refs.container.getDOMNode();
_.each(containerNode.childNodes, function(child) {
// set drop down width to length of current label
const label = child.getElementsByClassName('mui-menu-label')[0];
const width = label.getBoundingClientRect().width + 40;
child.style.width = width + 'px';
});
}
});
module.exports = TopologyOptions;
@@ -1,6 +1,7 @@
const keymirror = require('keymirror');
module.exports = keymirror({
CHANGE_TOPOLOGY_OPTION: null,
CLICK_CLOSE_DETAILS: null,
CLICK_NODE: null,
CLICK_TOPOLOGY: null,
@@ -28,6 +28,18 @@ describe('AppStore', function() {
// actions
const ChangeTopologyOptionAction = {
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
option: 'option1',
value: 'on'
};
const ChangeTopologyOptionAction2 = {
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
option: 'option1',
value: 'off'
};
const ClickNodeAction = {
type: ActionTypes.CLICK_NODE,
nodeId: 'n1'
@@ -88,6 +100,12 @@ describe('AppStore', function() {
topologies: [{
url: '/topo1',
name: 'Topo1',
options: {
option1: [
{value: 'on'},
{value: 'off', default: true}
]
},
sub_topologies: [{
url: '/topo1-grouped',
name: 'topo 1 grouped'
@@ -122,6 +140,7 @@ describe('AppStore', function() {
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getCurrentTopology().name).toBe('Topo1');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1');
expect(AppStore.getCurrentTopologyOptions().option1).toBeDefined();
});
it('get sub-topology', function() {
@@ -131,6 +150,32 @@ describe('AppStore', function() {
expect(AppStore.getTopologies().length).toBe(1);
expect(AppStore.getCurrentTopology().name).toBe('topo 1 grouped');
expect(AppStore.getCurrentTopologyUrl()).toBe('/topo1-grouped');
expect(AppStore.getCurrentTopologyOptions()).toBeUndefined();
});
// topology options
it('changes topology option', function() {
// default options
registeredCallback(ReceiveTopologiesAction);
registeredCallback(ClickTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
// turn on
registeredCallback(ChangeTopologyOptionAction);
expect(AppStore.getActiveTopologyOptions().option1).toBe('on');
expect(AppStore.getAppState().topologyOptions.option1).toBe('on');
// turn off
registeredCallback(ChangeTopologyOptionAction2);
expect(AppStore.getActiveTopologyOptions().option1).toBe('off');
expect(AppStore.getAppState().topologyOptions.option1).toBe('off');
// other topology w/o options
registeredCallback(ClickSubTopologyAction);
expect(AppStore.getActiveTopologyOptions().option1).toBeUndefined();
expect(AppStore.getAppState().topologyOptions.option1).toBeUndefined();
});
// nodes delta
@@ -166,12 +211,10 @@ describe('AppStore', function() {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getAppState())
.toEqual({"topologyId":"topo1","selectedNodeId": null});
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
registeredCallback(ClickNodeAction);
expect(AppStore.getAppState())
.toEqual({"topologyId":"topo1","selectedNodeId": 'n1'});
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
// go back in browsing
RouteAction.state = {"topologyId":"topo1","selectedNodeId": null};
@@ -185,16 +228,16 @@ describe('AppStore', function() {
registeredCallback(ClickTopologyAction);
registeredCallback(ReceiveNodesDeltaAction);
expect(AppStore.getAppState())
.toEqual({"topologyId":"topo1","selectedNodeId": null});
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
expect(AppStore.getAppState().topologyId).toEqual('topo1');
registeredCallback(ClickNodeAction);
expect(AppStore.getAppState())
.toEqual({"topologyId":"topo1","selectedNodeId": 'n1'});
expect(AppStore.getAppState().selectedNodeId).toEqual('n1');
expect(AppStore.getAppState().topologyId).toEqual('topo1');
registeredCallback(ClickSubTopologyAction);
expect(AppStore.getAppState())
.toEqual({"topologyId":"topo1-grouped","selectedNodeId": null});
expect(AppStore.getAppState().selectedNodeId).toEqual(null);
expect(AppStore.getAppState().topologyId).toEqual('topo1-grouped');
});
// connection errors
+53 -9
View File
@@ -43,6 +43,8 @@ function makeNode(node) {
// Initial values
let activeTopologyOptions = {};
let currentTopology = null;
let currentTopologyId = 'containers';
let errorUrl = null;
let version = '';
@@ -54,33 +56,60 @@ let selectedNodeId = null;
let topologies = [];
let websocketClosed = true;
function setTopology(topologyId) {
currentTopologyId = topologyId;
currentTopology = findCurrentTopology(topologies, topologyId);
}
function setDefaultTopologyOptions() {
activeTopologyOptions = {};
if (currentTopology) {
_.each(currentTopology.options, function(items, option) {
_.each(items, function(item) {
if (item.default === true) {
activeTopologyOptions[option] = item.value;
}
});
});
}
}
// Store API
const AppStore = assign({}, EventEmitter.prototype, {
CHANGE_EVENT: 'change',
// keep at the top
getAppState: function() {
return {
topologyId: currentTopologyId,
selectedNodeId: this.getSelectedNodeId()
selectedNodeId: this.getSelectedNodeId(),
topologyOptions: this.getActiveTopologyOptions()
};
},
getActiveTopologyOptions: function() {
return activeTopologyOptions;
},
getCurrentTopology: function() {
return findCurrentTopology(topologies, currentTopologyId);
if (!currentTopology) {
currentTopology = setTopology(currentTopologyId);
}
return currentTopology;
},
getCurrentTopologyId: function() {
return currentTopologyId;
},
getCurrentTopologyUrl: function() {
const topology = this.getCurrentTopology();
getCurrentTopologyOptions: function() {
return currentTopology && currentTopology.options;
},
if (topology) {
return topology.url;
}
getCurrentTopologyUrl: function() {
return currentTopology && currentTopology.url;
},
getErrorUrl: function() {
@@ -156,6 +185,14 @@ const AppStore = assign({}, EventEmitter.prototype, {
AppStore.registeredCallback = function(payload) {
switch (payload.type) {
case ActionTypes.CHANGE_TOPOLOGY_OPTION:
if (activeTopologyOptions[payload.option] !== payload.value) {
nodes = nodes.clear();
}
activeTopologyOptions[payload.option] = payload.value;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
case ActionTypes.CLICK_CLOSE_DETAILS:
selectedNodeId = null;
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -169,7 +206,8 @@ AppStore.registeredCallback = function(payload) {
case ActionTypes.CLICK_TOPOLOGY:
selectedNodeId = null;
if (payload.topologyId !== currentTopologyId) {
currentTopologyId = payload.topologyId;
setTopology(payload.topologyId);
setDefaultTopologyOptions();
nodes = nodes.clear();
}
AppStore.emit(AppStore.CHANGE_EVENT);
@@ -261,6 +299,10 @@ AppStore.registeredCallback = function(payload) {
case ActionTypes.RECEIVE_TOPOLOGIES:
errorUrl = null;
topologies = payload.topologies;
if (!currentTopology) {
setTopology(currentTopologyId);
setDefaultTopologyOptions();
}
AppStore.emit(AppStore.CHANGE_EVENT);
break;
@@ -274,8 +316,10 @@ AppStore.registeredCallback = function(payload) {
if (currentTopologyId !== payload.state.topologyId) {
nodes = nodes.clear();
}
currentTopologyId = payload.state.topologyId;
setTopology(payload.state.topologyId);
setDefaultTopologyOptions();
selectedNodeId = payload.state.selectedNodeId;
activeTopologyOptions = payload.state.topologyOptions || activeTopologyOptions;
AppStore.emit(AppStore.CHANGE_EVENT);
break;
+21 -11
View File
@@ -1,3 +1,4 @@
const _ = require('lodash');
const debug = require('debug')('scope:web-api-utils');
const reqwest = require('reqwest');
@@ -14,17 +15,19 @@ const updateFrequency = '5s';
let socket;
let reconnectTimer = 0;
let currentUrl = null;
let currentOptions = null;
let topologyTimer = 0;
let apiDetailsTimer = 0;
function createWebsocket(topologyUrl) {
function createWebsocket(topologyUrl, optionsQuery) {
if (socket) {
socket.onclose = null;
socket.onerror = null;
socket.close();
}
socket = new WebSocket(WS_URL + topologyUrl + '/ws?t=' + updateFrequency);
socket = new WebSocket(WS_URL + topologyUrl
+ '/ws?t=' + updateFrequency + '&' + optionsQuery);
socket.onopen = function() {
AppActions.openWebsocket();
@@ -34,7 +37,7 @@ function createWebsocket(topologyUrl) {
clearTimeout(reconnectTimer);
socket = null;
AppActions.closeWebsocket();
debug('Closed websocket to ' + currentUrl);
debug('Closed websocket to ' + topologyUrl);
reconnectTimer = setTimeout(function() {
createWebsocket(topologyUrl);
@@ -42,7 +45,7 @@ function createWebsocket(topologyUrl) {
};
socket.onerror = function() {
debug('Error in websocket to ' + currentUrl);
debug('Error in websocket to ' + topologyUrl);
AppActions.receiveError(currentUrl);
};
@@ -52,8 +55,6 @@ function createWebsocket(topologyUrl) {
AppActions.receiveNodesDelta(msg);
}
};
currentUrl = topologyUrl;
}
/* keep URLs relative */
@@ -75,6 +76,19 @@ function getTopologies() {
});
}
function getTopology(topologyUrl, options) {
const optionsQuery = _.map(options, function(value, param) {
return param + '=' + value;
}).join('&');
// only recreate websocket if url changed
if (topologyUrl && (topologyUrl !== currentUrl || currentOptions !== optionsQuery)) {
createWebsocket(topologyUrl, optionsQuery);
currentUrl = topologyUrl;
currentOptions = optionsQuery;
}
}
function getNodeDetails(topologyUrl, nodeId) {
if (topologyUrl && nodeId) {
const url = [topologyUrl, encodeURIComponent(nodeId)].join('/').substr(1);
@@ -115,10 +129,6 @@ module.exports = {
getApiDetails: getApiDetails,
getNodesDelta: function(topologyUrl) {
if (topologyUrl && topologyUrl !== currentUrl) {
createWebsocket(topologyUrl);
}
}
getNodesDelta: getTopology
};
+20
View File
@@ -130,6 +130,10 @@ body {
}
.topology-options {
margin-top: -16px;
}
.status {
float: right;
margin-top: 14px;
@@ -269,6 +273,22 @@ body {
height: 100%;
}
.mui-drop-down-menu {
.mui-menu-control {
.mui-menu-label {
font-size: 12px;
}
.mui-menu-control-underline {
border-top: none;
}
.mui-menu-control-bg {
background-color: transparent;
}
}
}
.node-details {
height: 100%;
width: 100%;
+97 -45
View File
@@ -1,6 +1,9 @@
package render
import (
"strings"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/report"
)
@@ -124,7 +127,8 @@ func (m Map) EdgeMetadata(rpt report.Report, srcRenderableID, dstRenderableID st
}
// CustomRenderer allow for mapping functions that recived the entire topology
// in one call - useful for functions that need to consider the entire graph
// in one call - useful for functions that need to consider the entire graph.
// We should minimise the use of this renderer type, as it is very inflexible.
type CustomRenderer struct {
RenderFunc func(RenderableNodes) RenderableNodes
Renderer
@@ -135,68 +139,116 @@ func (c CustomRenderer) Render(rpt report.Report) RenderableNodes {
return c.RenderFunc(c.Renderer.Render(rpt))
}
// IsConnected is the key added to Node.Metadata by ColorConnected
// to indicate a node has an edge pointing to it or from it
const IsConnected = "is_connected"
// OnlyConnected filters out unconnected RenderedNodes
func OnlyConnected(input RenderableNodes) RenderableNodes {
output := RenderableNodes{}
for id, node := range ColorConnected(input) {
if _, ok := node.Node.Metadata[IsConnected]; ok {
output[id] = node
}
}
return output
}
// FilterUnconnected produces a renderer that filters unconnected nodes
// from the given renderer
func FilterUnconnected(r Renderer) Renderer {
return CustomRenderer{
RenderFunc: OnlyConnected,
Renderer: r,
}
}
// ColorConnected colors nodes with the IsConnected key if
// they have edges to or from them.
func ColorConnected(input RenderableNodes) RenderableNodes {
connected := map[string]struct{}{}
void := struct{}{}
func ColorConnected(r Renderer) Renderer {
return CustomRenderer{
Renderer: r,
RenderFunc: func(input RenderableNodes) RenderableNodes {
connected := map[string]struct{}{}
void := struct{}{}
for id, node := range input {
if len(node.Adjacency) == 0 {
continue
}
for id, node := range input {
if len(node.Adjacency) == 0 {
continue
}
connected[id] = void
for _, id := range node.Adjacency {
connected[id] = void
}
connected[id] = void
for _, id := range node.Adjacency {
connected[id] = void
}
}
for id := range connected {
node := input[id]
node.Metadata[IsConnected] = "true"
input[id] = node
}
return input
},
}
for id := range connected {
node := input[id]
node.Node.Metadata[IsConnected] = "true"
input[id] = node
}
return input
}
// Filter removes nodes from a view based on a predicate.
type Filter struct {
Renderer
f func(RenderableNode) bool
FilterFunc func(RenderableNode) bool
}
// Render implements Renderer
func (f Filter) Render(rpt report.Report) RenderableNodes {
output := RenderableNodes{}
for id, node := range f.Renderer.Render(rpt) {
if f.f(node) {
if f.FilterFunc(node) {
output[id] = node
}
}
// Deleted nodes also need to be cut as destinations in adjacency lists.
for id, node := range output {
newAdjacency := make(report.IDList, 0, len(node.Adjacency))
for _, dstID := range node.Adjacency {
if _, ok := output[dstID]; ok {
newAdjacency = newAdjacency.Add(dstID)
}
}
node.Adjacency = newAdjacency
output[id] = node
}
return output
}
// IsConnected is the key added to Node.Metadata by ColorConnected
// to indicate a node has an edge pointing to it or from it
const IsConnected = "is_connected"
// FilterUnconnected produces a renderer that filters unconnected nodes
// from the given renderer
func FilterUnconnected(r Renderer) Renderer {
return Filter{
Renderer: ColorConnected(r),
FilterFunc: func(node RenderableNode) bool {
_, ok := node.Metadata[IsConnected]
return ok
},
}
}
// FilterSystem is a Renderer which filters out system nodes.
func FilterSystem(r Renderer) Renderer {
return Filter{
Renderer: r,
FilterFunc: func(node RenderableNode) bool {
containerName := node.Metadata[docker.ContainerName]
if _, ok := systemContainerNames[containerName]; ok {
return false
}
imagePrefix := strings.SplitN(node.Metadata[docker.ImageName], ":", 2)[0] // :(
if _, ok := systemImagePrefixes[imagePrefix]; ok {
return false
}
if node.Metadata[docker.LabelPrefix+"works.weave.role"] == "system" {
return false
}
return true
},
}
}
var systemContainerNames = map[string]struct{}{
"weavescope": {},
"weavedns": {},
"weave": {},
"weaveproxy": {},
"weaveexec": {},
"ecs-agent": {},
}
var systemImagePrefixes = map[string]struct{}{
"weaveworks/scope": {},
"weaveworks/weavedns": {},
"weaveworks/weave": {},
"weaveworks/weaveproxy": {},
"weaveworks/weaveexec": {},
"amazon/amazon-ecs-agent": {},
}
+23 -1
View File
@@ -187,7 +187,29 @@ func TestFilterRender(t *testing.T) {
}
have := expected.Sterilize(renderer.Render(report.MakeReport()))
if !reflect.DeepEqual(want, have) {
t.Errorf("want %+v, have %+v", want, have)
t.Error(test.Diff(want, have))
}
}
func TestFilterRender2(t *testing.T) {
// Test adjacencies are removed for filtered nodes.
renderer := render.Filter{
FilterFunc: func(node render.RenderableNode) bool {
return node.ID != "bar"
},
Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{
"foo": {ID: "foo", Node: report.MakeNode().WithAdjacent("bar")},
"bar": {ID: "bar", Node: report.MakeNode().WithAdjacent("foo")},
"baz": {ID: "baz", Node: report.MakeNode()},
}},
}
want := render.RenderableNodes{
"foo": {ID: "foo", Node: report.MakeNode()},
"baz": {ID: "baz", Node: report.MakeNode()},
}
have := expected.Sterilize(renderer.Render(report.MakeReport()))
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
+9
View File
@@ -125,6 +125,15 @@ func (rn RenderableNode) Copy() RenderableNode {
// RenderableNodes is a set of RenderableNodes
type RenderableNodes map[string]RenderableNode
// Copy produces a deep copy of the RenderableNodes
func (rns RenderableNodes) Copy() RenderableNodes {
result := RenderableNodes{}
for key, value := range rns {
result[key] = value.Copy()
}
return result
}
// Merge merges two sets of RenderableNodes, returning a new set.
func (rns RenderableNodes) Merge(other RenderableNodes) RenderableNodes {
result := RenderableNodes{}
+2 -5
View File
@@ -85,15 +85,12 @@ var ContainerRenderer = MakeReduce(
// but we need to be careful to ensure we only include each edge once, by only
// including the ProcessRenderer once.
Renderer: Filter{
f: func(n RenderableNode) bool {
FilterFunc: func(n RenderableNode) bool {
_, inContainer := n.Node.Metadata[docker.ContainerID]
_, isConnected := n.Node.Metadata[IsConnected]
return inContainer || isConnected
},
Renderer: CustomRenderer{
RenderFunc: ColorConnected,
Renderer: ProcessRenderer,
},
Renderer: ColorConnected(ProcessRenderer),
},
},
+14
View File
@@ -4,6 +4,7 @@ import (
"reflect"
"testing"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/render/expected"
"github.com/weaveworks/scope/test"
@@ -33,6 +34,19 @@ func TestContainerRenderer(t *testing.T) {
}
}
func TestContainerFilterRenderer(t *testing.T) {
// tag on of the containers in the topology and ensure
// it is filtered out correctly.
input := test.Report.Copy()
input.Container.Nodes[test.ClientContainerNodeID].Metadata[docker.LabelPrefix+"works.weave.role"] = "system"
have := expected.Sterilize(render.FilterSystem(render.ContainerWithImageNameRenderer{}).Render(input))
want := expected.RenderedContainers.Copy()
delete(want, test.ClientContainerID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
func TestContainerImageRenderer(t *testing.T) {
have := expected.Sterilize(render.ContainerImageRenderer.Render(test.Report))
want := expected.RenderedContainerImages