Skip to content

Commit

Permalink
Add docker lifecycle controls, containers in states other that runnin…
Browse files Browse the repository at this point in the history
…g, and a filter for those containers.

Also add integration test for container controls.
  • Loading branch information
tomwilkie committed Nov 6, 2015
1 parent 76d3433 commit 8f957c4
Show file tree
Hide file tree
Showing 13 changed files with 530 additions and 235 deletions.
36 changes: 18 additions & 18 deletions app/api_topologies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}},
},
Expand All @@ -33,14 +33,25 @@ 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},
}},
},
}
)

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(
Expand All @@ -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{
Expand All @@ -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",
Expand Down Expand Up @@ -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...)
}
Expand Down
22 changes: 22 additions & 0 deletions integration/410_container_control_test.sh
Original file line number Diff line number Diff line change
@@ -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
38 changes: 37 additions & 1 deletion probe/docker/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -69,6 +76,8 @@ type ClientConn interface {

// Container represents a Docker container
type Container interface {
UpdateState(*docker.Container)

ID() string
Image() string
PID() int
Expand All @@ -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 {
Expand Down Expand Up @@ -231,18 +248,37 @@ 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, "/"),
ContainerCreated: c.container.Created.Format(time.RFC822),
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"})
Expand Down
83 changes: 83 additions & 0 deletions probe/docker/controls.go
Original file line number Diff line number Diff line change
@@ -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)
}
38 changes: 38 additions & 0 deletions probe/docker/controls_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
Loading

0 comments on commit 8f957c4

Please sign in to comment.