diff --git a/app/api_topologies.go b/app/api_topologies.go index 7361fa91c3..3d9d68343c 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -23,7 +23,7 @@ var ( renderer: render.PodRenderer, Name: "Pods", Options: map[string][]APITopologyOption{"system": { - {"show", "System containers shown", false, nop}, + {"show", "System containers shown", false, render.FilterNoop}, {"hide", "System containers hidden", true, render.FilterSystem}, }}, }, @@ -33,7 +33,7 @@ var ( renderer: render.PodServiceRenderer, Name: "by service", Options: map[string][]APITopologyOption{"system": { - {"show", "System containers shown", false, nop}, + {"show", "System containers shown", false, render.FilterNoop}, {"hide", "System containers hidden", true, render.FilterSystem}, }}, }, @@ -41,6 +41,17 @@ var ( ) func init() { + containerFilters := map[string][]APITopologyOption{ + "system": { + {"show", "System containers shown", false, render.FilterNoop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }, + "stopped": { + {"show", "Stopped containers shown", false, render.FilterNoop}, + {"hide", "Stopped containers hidden", true, render.FilterStopped}, + }, + } + // Topology option labels should tell the current state. The first item must // be the verb to get to that state topologyRegistry.add( @@ -51,7 +62,7 @@ func init() { Options: map[string][]APITopologyOption{"unconnected": { // 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", true, nop}, + {"hide", "Unconnected nodes hidden", true, render.FilterNoop}, }}, }, APITopologyDesc{ @@ -61,37 +72,28 @@ func init() { Name: "by name", Options: map[string][]APITopologyOption{"unconnected": { // Ditto above. - {"hide", "Unconnected nodes hidden", true, nop}, + {"hide", "Unconnected nodes hidden", true, render.FilterNoop}, }}, }, APITopologyDesc{ id: "containers", renderer: render.ContainerWithImageNameRenderer, Name: "Containers", - Options: map[string][]APITopologyOption{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, + Options: containerFilters, }, APITopologyDesc{ id: "containers-by-image", parent: "containers", renderer: render.ContainerImageRenderer, Name: "by image", - Options: map[string][]APITopologyOption{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, + Options: containerFilters, }, APITopologyDesc{ id: "containers-by-hostname", parent: "containers", renderer: render.ContainerHostnameRenderer, Name: "by hostname", - Options: map[string][]APITopologyOption{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, + Options: containerFilters, }, APITopologyDesc{ id: "hosts", @@ -226,8 +228,6 @@ func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStat } } -func nop(r render.Renderer) render.Renderer { return r } - func (r *registry) enableKubernetesTopologies() { r.add(kubernetesTopologies...) } diff --git a/integration/410_container_control_test.sh b/integration/410_container_control_test.sh new file mode 100755 index 0000000000..b96e434b9e --- /dev/null +++ b/integration/410_container_control_test.sh @@ -0,0 +1,22 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Test container controls" + +weave_on $HOST1 launch +scope_on $HOST1 launch + +CID=$(weave_on $HOST1 run -dti --name alpine alpine /bin/sh) + +wait_for_containers $HOST1 60 alpine + +assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "true" +PROBEID=$(docker_on $HOST1 logs weavescope 2>&1 | grep "probe starting" | sed -n 's/^.*ID \([0-9a-f]*\)$/\1/p') +HOSTID=$(echo $HOST1 | cut -d"." -f1) +assert_raises "curl -f -X POST 'http://$HOST1:4040/api/control/$PROBEID/$HOSTID;$CID/docker_stop_container'" + +sleep 5 +assert "docker_on $HOST1 inspect --format='{{.State.Running}}' alpine" "false" + +scope_end_suite diff --git a/probe/docker/container.go b/probe/docker/container.go index af721b8cd3..8806310ffe 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -29,6 +29,7 @@ const ( ContainerIPs = "docker_container_ips" ContainerHostname = "docker_container_hostname" ContainerIPsWithScopes = "docker_container_ips_with_scopes" + ContainerState = "docker_container_state" NetworkRxDropped = "network_rx_dropped" NetworkRxBytes = "network_rx_bytes" @@ -49,6 +50,12 @@ const ( CPUTotalUsage = "cpu_total_usage" CPUUsageInKernelmode = "cpu_usage_in_kernelmode" CPUSystemCPUUsage = "cpu_system_cpu_usage" + + StateRunning = "running" + StateStopped = "stopped" + StatePaused = "paused" + + stopTimeout = 10 ) // Exported for testing @@ -69,6 +76,8 @@ type ClientConn interface { // Container represents a Docker container type Container interface { + UpdateState(*docker.Container) + ID() string Image() string PID() int @@ -88,7 +97,15 @@ type container struct { // NewContainer creates a new Container func NewContainer(c *docker.Container) Container { - return &container{container: c} + return &container{ + container: c, + } +} + +func (c *container) UpdateState(container *docker.Container) { + c.Lock() + defer c.Unlock() + c.container = container } func (c *container) ID() string { @@ -231,6 +248,15 @@ func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node { ipsWithScopes = append(ipsWithScopes, report.MakeScopedAddressNodeID(hostID, ip)) } + var state string + if c.container.State.Paused { + state = StatePaused + } else if c.container.State.Running { + state = StateRunning + } else { + state = StateStopped + } + result := report.MakeNodeWith(map[string]string{ ContainerID: c.ID(), ContainerName: strings.TrimPrefix(c.container.Name, "/"), @@ -238,11 +264,21 @@ func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node { ContainerCommand: c.container.Path + " " + strings.Join(c.container.Args, " "), ImageID: c.container.Image, ContainerHostname: c.Hostname(), + ContainerState: state, }).WithSets(report.Sets{ ContainerPorts: c.ports(localAddrs), ContainerIPs: report.MakeStringSet(ips...), ContainerIPsWithScopes: report.MakeStringSet(ipsWithScopes...), }) + + if c.container.State.Paused { + result = result.WithControls(UnpauseContainer) + } else if c.container.State.Running { + result = result.WithControls(RestartContainer, StopContainer, PauseContainer) + } else { + result = result.WithControls(StartContainer) + } + AddLabels(result, c.container.Config.Labels) if c.latestStats == nil { diff --git a/probe/docker/container_linux_test.go b/probe/docker/container_test.go similarity index 94% rename from probe/docker/container_linux_test.go rename to probe/docker/container_test.go index 3a73cceaad..fa353543fe 100644 --- a/probe/docker/container_linux_test.go +++ b/probe/docker/container_test.go @@ -74,11 +74,12 @@ func TestContainer(t *testing.T) { "docker_label_foo1": "bar1", "docker_label_foo2": "bar2", "memory_usage": "12345", + "docker_container_state": "running", }).WithSets(report.Sets{ "docker_container_ports": report.MakeStringSet("1.2.3.4:80->80/tcp", "81/tcp"), "docker_container_ips": report.MakeStringSet("1.2.3.4"), "docker_container_ips_with_scopes": report.MakeStringSet("scope;1.2.3.4"), - }) + }).WithControls(docker.RestartContainer, docker.StopContainer, docker.PauseContainer) test.Poll(t, 100*time.Millisecond, want, func() interface{} { node := c.GetNode("scope", []net.IP{}) for k, v := range node.Metadata { @@ -93,7 +94,7 @@ func TestContainer(t *testing.T) { t.Errorf("%s != baz", c.Image()) } if c.PID() != 1 { - t.Errorf("%s != 1", c.PID()) + t.Errorf("%d != 1", c.PID()) } if have := docker.ExtractContainerIPs(c.GetNode("", []net.IP{})); !reflect.DeepEqual(have, []string{"1.2.3.4"}) { t.Errorf("%v != %v", have, []string{"1.2.3.4"}) diff --git a/probe/docker/controls.go b/probe/docker/controls.go new file mode 100644 index 0000000000..0db14c3180 --- /dev/null +++ b/probe/docker/controls.go @@ -0,0 +1,83 @@ +package docker + +import ( + "log" + + "github.com/weaveworks/scope/probe/controls" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/xfer" +) + +// Control IDs used by the docker intergation. +const ( + StopContainer = "docker_stop_container" + StartContainer = "docker_start_container" + RestartContainer = "docker_restart_container" + PauseContainer = "docker_pause_container" + UnpauseContainer = "docker_unpause_container" + + waitTime = 10 +) + +func (r *registry) stopContainer(req xfer.Request) xfer.Response { + log.Printf("Stopping container %s", req.NodeID) + + _, containerID, ok := report.ParseContainerNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + + return xfer.ResponseError(r.client.StopContainer(containerID, waitTime)) +} + +func (r *registry) startContainer(req xfer.Request) xfer.Response { + log.Printf("Starting container %s", req.NodeID) + + _, containerID, ok := report.ParseContainerNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + + return xfer.ResponseError(r.client.StartContainer(containerID, nil)) +} + +func (r *registry) restartContainer(req xfer.Request) xfer.Response { + log.Printf("Restarting container %s", req.NodeID) + + _, containerID, ok := report.ParseContainerNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + + return xfer.ResponseError(r.client.RestartContainer(containerID, waitTime)) +} + +func (r *registry) pauseContainer(req xfer.Request) xfer.Response { + log.Printf("Pausing container %s", req.NodeID) + + _, containerID, ok := report.ParseContainerNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + + return xfer.ResponseError(r.client.PauseContainer(containerID)) +} + +func (r *registry) unpauseContainer(req xfer.Request) xfer.Response { + log.Printf("Unpausing container %s", req.NodeID) + + _, containerID, ok := report.ParseContainerNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + + return xfer.ResponseError(r.client.UnpauseContainer(containerID)) +} + +func (r *registry) registerControls() { + controls.Register(StopContainer, r.stopContainer) + controls.Register(StartContainer, r.startContainer) + controls.Register(RestartContainer, r.restartContainer) + controls.Register(PauseContainer, r.pauseContainer) + controls.Register(UnpauseContainer, r.unpauseContainer) +} diff --git a/probe/docker/controls_test.go b/probe/docker/controls_test.go new file mode 100644 index 0000000000..0a6da47676 --- /dev/null +++ b/probe/docker/controls_test.go @@ -0,0 +1,38 @@ +package docker_test + +import ( + "reflect" + "testing" + "time" + + "github.com/weaveworks/scope/probe/controls" + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/xfer" +) + +func TestControls(t *testing.T) { + mdc := newMockClient() + setupStubs(mdc, func() { + registry, _ := docker.NewRegistry(10 * time.Second) + defer registry.Stop() + + for _, tc := range []struct{ command, result string }{ + {docker.StopContainer, "stopped"}, + {docker.StartContainer, "started"}, + {docker.RestartContainer, "restarted"}, + {docker.PauseContainer, "paused"}, + {docker.UnpauseContainer, "unpaused"}, + } { + result := controls.HandleControlRequest(xfer.Request{ + Control: tc.command, + NodeID: report.MakeContainerNodeID("", "a1b2c3d4e5"), + }) + if !reflect.DeepEqual(result, xfer.Response{ + Error: tc.result, + }) { + t.Error(result) + } + } + }) +} diff --git a/probe/docker/registry.go b/probe/docker/registry.go index 0fad353a0b..3c21bd4e76 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -10,9 +10,13 @@ import ( // Consts exported for testing. const ( - StartEvent = "start" - DieEvent = "die" - endpoint = "unix:///var/run/docker.sock" + CreateEvent = "create" + DestroyEvent = "destroy" + StartEvent = "start" + DieEvent = "die" + PauseEvent = "pause" + UnpauseEvent = "unpause" + endpoint = "unix:///var/run/docker.sock" ) // Vars exported for testing. @@ -47,6 +51,11 @@ type Client interface { ListImages(docker_client.ListImagesOptions) ([]docker_client.APIImages, error) AddEventListener(chan<- *docker_client.APIEvents) error RemoveEventListener(chan *docker_client.APIEvents) error + StopContainer(string, uint) error + StartContainer(string, *docker_client.HostConfig) error + RestartContainer(string, uint) error + PauseContainer(string) error + UnpauseContainer(string) error } func newDockerClient(endpoint string) (Client, error) { @@ -70,6 +79,7 @@ func NewRegistry(interval time.Duration) (Registry, error) { quit: make(chan chan struct{}), } + r.registerControls() go r.loop() return r, nil } @@ -170,9 +180,7 @@ func (r *registry) updateContainers() error { } for _, apiContainer := range apiContainers { - if err := r.addContainer(apiContainer.ID); err != nil { - return err - } + r.updateContainerState(apiContainer.ID) } return nil @@ -197,56 +205,54 @@ func (r *registry) updateImages() error { func (r *registry) handleEvent(event *docker_client.APIEvents) { switch event.Status { - case DieEvent: - containerID := event.ID - r.removeContainer(containerID) - - case StartEvent: - containerID := event.ID - if err := r.addContainer(containerID); err != nil { - log.Printf("docker registry: %s", err) - } + case CreateEvent, StartEvent, DieEvent, DestroyEvent, PauseEvent, UnpauseEvent: + r.updateContainerState(event.ID) } } -func (r *registry) addContainer(containerID string) error { +func (r *registry) updateContainerState(containerID string) { + r.Lock() + defer r.Unlock() + dockerContainer, err := r.client.InspectContainer(containerID) if err != nil { // Don't spam the logs if the container was short lived - if _, ok := err.(*docker_client.NoSuchContainer); ok { - return nil + if _, ok := err.(*docker_client.NoSuchContainer); !ok { + log.Printf("Error processing event for container %s: %v", containerID, err) + return } - return err - } - - if !dockerContainer.State.Running { - // We get events late, and the containers sometimes have already - // stopped. Not an error, so don't return it. - return nil - } - - r.Lock() - defer r.Unlock() - - c := NewContainerStub(dockerContainer) - r.containers[containerID] = c - r.containersByPID[dockerContainer.State.Pid] = c - return c.StartGatheringStats() -} + // Container doesn't exist anymore, so lets stop and remove it + container, ok := r.containers[containerID] + if !ok { + return + } -func (r *registry) removeContainer(containerID string) { - r.Lock() - defer r.Unlock() + delete(r.containers, containerID) + delete(r.containersByPID, container.PID()) + container.StopGatheringStats() + return + } - container, ok := r.containers[containerID] + // Container exists, ensure we have it + c, ok := r.containers[containerID] if !ok { - return + c = NewContainerStub(dockerContainer) + r.containers[containerID] = c + r.containersByPID[dockerContainer.State.Pid] = c + } else { + c.UpdateState(dockerContainer) } - delete(r.containers, containerID) - delete(r.containersByPID, container.PID()) - container.StopGatheringStats() + // And finally, ensure we gather stats for it + if dockerContainer.State.Running { + if err := c.StartGatheringStats(); err != nil { + log.Printf("Error gather stats for container: %s", containerID) + return + } + } else { + c.StopGatheringStats() + } } // LockedPIDLookup runs f under a read lock, and gives f a function for @@ -272,6 +278,13 @@ func (r *registry) WalkContainers(f func(Container)) { } } +func (r *registry) getContainer(id string) (Container, bool) { + r.RLock() + defer r.RUnlock() + c, ok := r.containers[id] + return c, ok +} + // WalkImages runs f on every image of running containers the registry // knows of. f may be run on the same image more than once. func (r *registry) WalkImages(f func(*docker_client.APIImages)) { diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go index b5ac445082..6361298fd7 100644 --- a/probe/docker/registry_test.go +++ b/probe/docker/registry_test.go @@ -1,6 +1,7 @@ package docker_test import ( + "fmt" "net" "runtime" "sort" @@ -19,6 +20,8 @@ type mockContainer struct { c *client.Container } +func (c *mockContainer) UpdateState(_ *client.Container) {} + func (c *mockContainer) ID() string { return c.c.ID } @@ -66,7 +69,11 @@ func (m *mockDockerClient) ListContainers(client.ListContainersOptions) ([]clien func (m *mockDockerClient) InspectContainer(id string) (*client.Container, error) { m.RLock() defer m.RUnlock() - return m.containers[id], nil + c, ok := m.containers[id] + if !ok { + return nil, &client.NoSuchContainer{} + } + return c, nil } func (m *mockDockerClient) ListImages(client.ListImagesOptions) ([]client.APIImages, error) { @@ -93,6 +100,26 @@ func (m *mockDockerClient) RemoveEventListener(events chan *client.APIEvents) er return nil } +func (m *mockDockerClient) StartContainer(_ string, _ *client.HostConfig) error { + return fmt.Errorf("started") +} + +func (m *mockDockerClient) StopContainer(_ string, _ uint) error { + return fmt.Errorf("stopped") +} + +func (m *mockDockerClient) RestartContainer(_ string, _ uint) error { + return fmt.Errorf("restarted") +} + +func (m *mockDockerClient) PauseContainer(_ string) error { + return fmt.Errorf("paused") +} + +func (m *mockDockerClient) UnpauseContainer(_ string) error { + return fmt.Errorf("unpaused") +} + func (m *mockDockerClient) send(event *client.APIEvents) { m.RLock() defer m.RUnlock() @@ -259,7 +286,7 @@ func TestRegistryEvents(t *testing.T) { mdc.apiContainers = []client.APIContainers{apiContainer1} delete(mdc.containers, "wiff") mdc.Unlock() - mdc.send(&client.APIEvents{Status: docker.DieEvent, ID: "wiff"}) + mdc.send(&client.APIEvents{Status: docker.DestroyEvent, ID: "wiff"}) runtime.Gosched() want := []docker.Container{&mockContainer{container1}} diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 01b6fbd7e7..8f7561efc0 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -43,6 +43,31 @@ func (r *Reporter) Report() (report.Report, error) { func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { result := report.MakeTopology() + result.Controls.AddControl(report.Control{ + ID: StopContainer, + Human: "Stop", + Icon: "fa-stop", + }) + result.Controls.AddControl(report.Control{ + ID: StartContainer, + Human: "Start", + Icon: "fa-play", + }) + result.Controls.AddControl(report.Control{ + ID: RestartContainer, + Human: "Restart", + Icon: "fa-repeat", + }) + result.Controls.AddControl(report.Control{ + ID: PauseContainer, + Human: "Pause", + Icon: "fa-pause", + }) + result.Controls.AddControl(report.Control{ + ID: UnpauseContainer, + Human: "Unpause", + Icon: "fa-play", + }) r.registry.WalkContainers(func(c Container) { nodeID := report.MakeContainerNodeID(r.hostID, c.ID()) diff --git a/probe/docker/reporter_test.go b/probe/docker/reporter_test.go index 5e765c5563..cbabba81bb 100644 --- a/probe/docker/reporter_test.go +++ b/probe/docker/reporter_test.go @@ -57,6 +57,33 @@ func TestReporter(t *testing.T) { docker.ImageID: "baz", }), }, + Controls: report.Controls{ + docker.RestartContainer: report.Control{ + ID: docker.RestartContainer, + Human: "Restart", + Icon: "fa-repeat", + }, + docker.StartContainer: report.Control{ + ID: docker.StartContainer, + Human: "Start", + Icon: "fa-play", + }, + docker.StopContainer: report.Control{ + ID: docker.StopContainer, + Human: "Stop", + Icon: "fa-stop", + }, + docker.PauseContainer: report.Control{ + ID: docker.PauseContainer, + Human: "Pause", + Icon: "fa-pause", + }, + docker.UnpauseContainer: report.Control{ + ID: docker.UnpauseContainer, + Human: "Unpause", + Icon: "fa-play", + }, + }, } want.ContainerImage = report.Topology{ Nodes: report.Nodes{ @@ -65,6 +92,7 @@ func TestReporter(t *testing.T) { docker.ImageName: "bang", }), }, + Controls: report.Controls{}, } reporter := docker.NewReporter(mockRegistryInstance, "") diff --git a/render/detailed_node.go b/render/detailed_node.go index 1396959b90..f9e8c1ba37 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -346,6 +346,7 @@ func processOriginTable(nmd report.Node, addHostTag bool, addContainerTag bool) func containerOriginTable(nmd report.Node, addHostTag bool) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ + {docker.ContainerState, "State"}, {docker.ContainerID, "ID"}, {docker.ImageID, "Image ID"}, {docker.ContainerPorts, "Ports"}, diff --git a/render/filters.go b/render/filters.go new file mode 100644 index 0000000000..22eb00d2a0 --- /dev/null +++ b/render/filters.go @@ -0,0 +1,190 @@ +package render + +import ( + "strings" + + "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/kubernetes" + "github.com/weaveworks/scope/report" +) + +// CustomRenderer allow for mapping functions that recived the entire topology +// 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 +} + +// Render implements Renderer +func (c CustomRenderer) Render(rpt report.Report) RenderableNodes { + return c.RenderFunc(c.Renderer.Render(rpt)) +} + +// ColorConnected colors nodes with the IsConnected key if +// they have edges to or from them. +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 + } + + 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 + }, + } +} + +// Filter removes nodes from a view based on a predicate. +type Filter struct { + Renderer + FilterFunc func(RenderableNode) bool +} + +// Render implements Renderer +func (f Filter) Render(rpt report.Report) RenderableNodes { + nodes, _ := f.render(rpt) + return nodes +} + +func (f Filter) render(rpt report.Report) (RenderableNodes, int) { + output := RenderableNodes{} + inDegrees := map[string]int{} + filtered := 0 + for id, node := range f.Renderer.Render(rpt) { + if f.FilterFunc(node) { + output[id] = node + inDegrees[id] = 0 + } else { + filtered++ + } + } + + // 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) + inDegrees[dstID]++ + } + } + node.Adjacency = newAdjacency + output[id] = node + } + + // Remove unconnected pseudo nodes, see #483. + for id, inDegree := range inDegrees { + if inDegree > 0 { + continue + } + node := output[id] + if !node.Pseudo || len(node.Adjacency) > 0 { + continue + } + delete(output, id) + filtered++ + } + return output, filtered +} + +// Stats implements Renderer +func (f Filter) Stats(rpt report.Report) Stats { + _, filtered := f.render(rpt) + var upstream = f.Renderer.Stats(rpt) + upstream.FilteredNodes += filtered + return upstream +} + +// 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 + }, + } +} + +// FilterNoop does nothing. +func FilterNoop(in Renderer) Renderer { + return in +} + +// FilterStopped filters out stopped containers. +func FilterStopped(r Renderer) Renderer { + return Filter{ + Renderer: r, + FilterFunc: func(node RenderableNode) bool { + containerState := node.Metadata[docker.ContainerState] + return containerState != docker.StateStopped + }, + } +} + +// 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 + } + if node.Metadata[kubernetes.Namespace] == "kube-system" { + return false + } + if strings.HasPrefix(node.Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"], "kube-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": {}, +} diff --git a/render/render.go b/render/render.go index 79db7a73a3..8f1de1f02e 100644 --- a/render/render.go +++ b/render/render.go @@ -1,10 +1,6 @@ package render import ( - "strings" - - "github.com/weaveworks/scope/probe/docker" - "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/report" ) @@ -155,168 +151,3 @@ func (m Map) EdgeMetadata(rpt report.Report, srcRenderableID, dstRenderableID st } return output } - -// CustomRenderer allow for mapping functions that recived the entire topology -// 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 -} - -// Render implements Renderer -func (c CustomRenderer) Render(rpt report.Report) RenderableNodes { - return c.RenderFunc(c.Renderer.Render(rpt)) -} - -// ColorConnected colors nodes with the IsConnected key if -// they have edges to or from them. -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 - } - - 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 - }, - } -} - -// Filter removes nodes from a view based on a predicate. -type Filter struct { - Renderer - FilterFunc func(RenderableNode) bool -} - -// Render implements Renderer -func (f Filter) Render(rpt report.Report) RenderableNodes { - nodes, _ := f.render(rpt) - return nodes -} - -func (f Filter) render(rpt report.Report) (RenderableNodes, int) { - output := RenderableNodes{} - inDegrees := map[string]int{} - filtered := 0 - for id, node := range f.Renderer.Render(rpt) { - if f.FilterFunc(node) { - output[id] = node - inDegrees[id] = 0 - } else { - filtered++ - } - } - - // 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) - inDegrees[dstID]++ - } - } - node.Adjacency = newAdjacency - output[id] = node - } - - // Remove unconnected pseudo nodes, see #483. - for id, inDegree := range inDegrees { - if inDegree > 0 { - continue - } - node := output[id] - if !node.Pseudo || len(node.Adjacency) > 0 { - continue - } - delete(output, id) - filtered++ - } - return output, filtered -} - -// Stats implements Renderer -func (f Filter) Stats(rpt report.Report) Stats { - _, filtered := f.render(rpt) - var upstream = f.Renderer.Stats(rpt) - upstream.FilteredNodes += filtered - return upstream -} - -// 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 - } - if node.Metadata[kubernetes.Namespace] == "kube-system" { - return false - } - if strings.HasPrefix(node.Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"], "kube-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": {}, -}