mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-23 22:36:24 +00:00
Add docker lifecycle controls, containers in states other that running, and a filter for those containers.
Also add integration test for container controls.
This commit is contained in:
+18
-18
@@ -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...)
|
||||
}
|
||||
|
||||
Executable
+22
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+57
-44
@@ -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
|
||||
}
|
||||
// Container doesn't exist anymore, so lets stop and remove it
|
||||
container, ok := r.containers[containerID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
c := NewContainerStub(dockerContainer)
|
||||
r.containers[containerID] = c
|
||||
r.containersByPID[dockerContainer.State.Pid] = c
|
||||
|
||||
return c.StartGatheringStats()
|
||||
}
|
||||
|
||||
func (r *registry) removeContainer(containerID string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
container, ok := r.containers[containerID]
|
||||
if !ok {
|
||||
delete(r.containers, containerID)
|
||||
delete(r.containersByPID, container.PID())
|
||||
container.StopGatheringStats()
|
||||
return
|
||||
}
|
||||
|
||||
delete(r.containers, containerID)
|
||||
delete(r.containersByPID, container.PID())
|
||||
container.StopGatheringStats()
|
||||
// Container exists, ensure we have it
|
||||
c, ok := r.containers[containerID]
|
||||
if !ok {
|
||||
c = NewContainerStub(dockerContainer)
|
||||
r.containers[containerID] = c
|
||||
r.containersByPID[dockerContainer.State.Pid] = c
|
||||
} else {
|
||||
c.UpdateState(dockerContainer)
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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": {},
|
||||
}
|
||||
@@ -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": {},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user