Files
weave-scope/render/container.go
Paul Bellamy 3d3aed2bb3 Fixing grouped node count for filtered children nodes
Squash of:

* We have to keep all the container hostnames until the end so we can
  count how many we've filtered

* Adding tests for ContainerHostnameRenderer and PodServiceRenderer with
  filters

* Because we filter on image name we need the image name before
  filtering

* Alternative approach to passing decorators.

* Refactor out some of the decorator capture

* Don't memoise decorated calls to Render

* Fixing filtered counts on containers topology

  Tricky, because we need the filters to be silent sometimes (when they're
  in the middle), but not when they're at the top, so we take the "top"
  filter's stats. However, this means we have to compose all
  user-specified filters into a single Filter layer, so we can get all
  stats.

  There are no more Silent filters, as all filters are silent (unless they
  are at the top).

  Additionally, I clarified some of the filters as their usage/terminology
  was inconsistent and confused. Now Filter(IsFoo, ...) *keeps* only nodes
  where IsFoo is true.
2016-04-28 12:23:43 +01:00

388 lines
12 KiB
Go

package render
import (
"fmt"
"net"
"regexp"
"strings"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/report"
)
// Constants are used in the tests.
const (
UncontainedID = "uncontained"
UncontainedMajor = "Uncontained"
// Topology for IPs so we can differentiate them at the end
IP = "IP"
)
// ContainerRenderer is a Renderer which produces a renderable container
// graph by merging the process graph and the container topology.
// NB We only want processes in container _or_ processes with network connections
// but we need to be careful to ensure we only include each edge once, by only
// including the ProcessRenderer once.
var ContainerRenderer = MakeFilter(
func(n report.Node) bool {
// Drop deleted containers
state, ok := n.Latest.Lookup(docker.ContainerState)
return !ok || state != docker.StateDeleted
},
MakeReduce(
MakeFilter(
func(n report.Node) bool {
// Drop unconnected pseudo nodes (could appear due to filtering)
_, isConnected := n.Latest.Lookup(IsConnected)
return n.Topology != Pseudo || isConnected
},
MakeMap(
MapProcess2Container,
ColorConnected(ProcessRenderer),
),
),
// This mapper brings in short lived connections by joining with container IPs.
// We need to be careful to ensure we only include each edge once. Edges brought in
// by the above renders will have a pid, so its enough to filter out any nodes with
// pids.
FilterUnconnected(MakeMap(
MapIP2Container,
MakeReduce(
MakeMap(
MapContainer2IP,
SelectContainer,
),
MakeMap(
MapEndpoint2IP,
SelectEndpoint,
),
),
)),
SelectContainer,
),
)
type containerWithHostIPsRenderer struct {
Renderer
}
// Render produces a process graph where the ips for host network mode are set
// to the host's IPs.
func (r containerWithHostIPsRenderer) Render(rpt report.Report, dct Decorator) report.Nodes {
containers := r.Renderer.Render(rpt, dct)
hosts := SelectHost.Render(rpt, dct)
outputs := report.Nodes{}
for id, c := range containers {
outputs[id] = c
networkMode, ok := c.Latest.Lookup(docker.ContainerNetworkMode)
if !ok || networkMode != docker.NetworkModeHost {
continue
}
h, ok := hosts[report.MakeHostNodeID(report.ExtractHostID(c))]
if !ok {
continue
}
newIPs := report.MakeStringSet()
hostNetworks, _ := h.Sets.Lookup(host.LocalNetworks)
for _, cidr := range hostNetworks {
if ip, _, err := net.ParseCIDR(cidr); err == nil {
newIPs = newIPs.Add(ip.String())
}
}
output := c.Copy()
output.Sets = c.Sets.Add(docker.ContainerIPs, newIPs)
outputs[id] = output
}
return outputs
}
// ContainerWithHostIPsRenderer is a Renderer which produces a container graph
// enriched with host IPs on containers where NetworkMode is Host
var ContainerWithHostIPsRenderer = containerWithHostIPsRenderer{ContainerRenderer}
type containerWithImageNameRenderer struct {
Renderer
}
// Render produces a process graph where the minor labels contain the
// container name, if found. It also merges the image node metadata into the
// container metadata.
func (r containerWithImageNameRenderer) Render(rpt report.Report, dct Decorator) report.Nodes {
containers := r.Renderer.Render(rpt, dct)
images := SelectContainerImage.Render(rpt, dct)
outputs := report.Nodes{}
for id, c := range containers {
outputs[id] = c
imageID, ok := c.Latest.Lookup(docker.ImageID)
if !ok {
continue
}
image, ok := images[report.MakeContainerImageNodeID(imageID)]
if !ok {
continue
}
output := c.Copy()
output.Latest = image.Latest.Merge(c.Latest)
outputs[id] = output
}
return outputs
}
// ContainerWithImageNameRenderer is a Renderer which produces a container
// graph where the ranks are the image names, not their IDs
var ContainerWithImageNameRenderer = ApplyDecorators(containerWithImageNameRenderer{ContainerWithHostIPsRenderer})
// ContainerImageRenderer is a Renderer which produces a renderable container
// image graph by merging the container graph and the container image topology.
var ContainerImageRenderer = FilterEmpty(report.Container,
MakeReduce(
MakeMap(
MapContainer2ContainerImage,
ContainerWithImageNameRenderer,
),
SelectContainerImage,
),
)
// ContainerHostnameRenderer is a Renderer which produces a renderable container
// by hostname graph..
var ContainerHostnameRenderer = FilterEmpty(report.Container,
MakeReduce(
MakeMap(
MapContainer2Hostname,
ContainerWithImageNameRenderer,
),
// Grab *all* the hostnames, so we can count the number which were empty
// for accurate stats.
MakeMap(
MapToEmpty,
MakeMap(
MapContainer2Hostname,
ContainerRenderer,
),
),
),
)
var portMappingMatch = regexp.MustCompile(`([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}):([0-9]+)->([0-9]+)/tcp`)
// MapEndpoint2IP maps endpoint nodes to their IP address, for joining
// with container nodes. We drop endpoint nodes with pids, as they
// will be joined to containers through the process topology, and we
// don't want to double count edges.
func MapEndpoint2IP(m report.Node, local report.Networks) report.Nodes {
// Don't include procspied connections, to prevent double counting
_, ok := m.Latest.Lookup(endpoint.Procspied)
if ok {
return report.Nodes{}
}
scope, addr, port, ok := report.ParseEndpointNodeID(m.ID)
if !ok {
return report.Nodes{}
}
if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) {
return report.Nodes{TheInternetID: theInternetNode(m)}
}
// We don't always know what port a container is listening on, and
// container-to-container communications can be unambiguously identified
// without ports. OTOH, connections to the host IPs which have been port
// mapped to a container can only be unambiguously identified with the port.
// So we need to emit two nodes, for two different cases.
id := report.MakeScopedEndpointNodeID(scope, addr, "")
idWithPort := report.MakeScopedEndpointNodeID(scope, addr, port)
return report.Nodes{
id: NewDerivedNode(id, m).WithTopology(IP),
idWithPort: NewDerivedNode(idWithPort, m).WithTopology(IP),
}
}
// MapContainer2IP maps container nodes to their IP addresses (outputs
// multiple nodes). This allows container to be joined directly with
// the endpoint topology.
func MapContainer2IP(m report.Node, _ report.Networks) report.Nodes {
containerID, ok := m.Latest.Lookup(docker.ContainerID)
if !ok {
return report.Nodes{}
}
result := report.Nodes{}
if addrs, ok := m.Sets.Lookup(docker.ContainerIPsWithScopes); ok {
for _, addr := range addrs {
scope, addr, ok := report.ParseAddressNodeID(addr)
if !ok {
continue
}
id := report.MakeScopedEndpointNodeID(scope, addr, "")
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
WithCounters(map[string]int{IP: 1})
}
}
// Also output all the host:port port mappings (see above comment).
// In this case we assume this doesn't need a scope, as they are for host IPs.
ports, _ := m.Sets.Lookup(docker.ContainerPorts)
for _, portMapping := range ports {
if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
ip, port := mapping[1], mapping[2]
id := report.MakeScopedEndpointNodeID("", ip, port)
result[id] = NewDerivedNode(id, m).
WithTopology(IP).
WithLatests(map[string]string{docker.ContainerID: containerID}).
WithCounters(map[string]int{IP: 1})
}
}
return result
}
// MapIP2Container maps IP nodes produced from MapContainer2IP back to
// container nodes. If there is more than one container with a given
// IP, it is dropped.
func MapIP2Container(n report.Node, _ report.Networks) report.Nodes {
// If an IP is shared between multiple containers, we can't
// reliably attribute an connection based on its IP
if count, _ := n.Counters.Lookup(IP); count > 1 {
return report.Nodes{}
}
// Propagate the internet pseudo node
if strings.HasSuffix(n.ID, TheInternetID) {
return report.Nodes{n.ID: n}
}
// If this node is not a container, exclude it.
// This excludes all the nodes we've dragged in from endpoint
// that we failed to join to a container.
containerID, ok := n.Latest.Lookup(docker.ContainerID)
if !ok {
return report.Nodes{}
}
id := report.MakeContainerNodeID(containerID)
return report.Nodes{
id: NewDerivedNode(id, n).
WithTopology(report.Container),
}
}
// MapProcess2Container maps process Nodes to container
// Nodes.
//
// If this function is given a node without a docker_container_id
// (including other pseudo nodes), it will produce an "Uncontained"
// pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a container graph to get that info.
func MapProcess2Container(n report.Node, _ report.Networks) report.Nodes {
// Propagate pseudo nodes
if n.Topology == Pseudo {
return report.Nodes{n.ID: n}
}
// Otherwise, if the process is not in a container, group it
// into an per-host "Uncontained" node. If for whatever reason
// this node doesn't have a host id in their nodemetadata, it'll
// all get grouped into a single uncontained node.
var (
id string
node report.Node
)
if containerID, ok := n.Latest.Lookup(docker.ContainerID); ok {
id = report.MakeContainerNodeID(containerID)
node = NewDerivedNode(id, n).WithTopology(report.Container)
} else {
id = MakePseudoNodeID(UncontainedID, report.ExtractHostID(n))
node = NewDerivedPseudoNode(id, n)
}
return report.Nodes{id: node}
}
// MapContainer2ContainerImage maps container Nodes to container
// image Nodes.
//
// If this function is given a node without a docker_image_id
// (including other pseudo nodes), it will produce an "Uncontained"
// pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a container graph to get that info.
func MapContainer2ContainerImage(n report.Node, _ report.Networks) report.Nodes {
// Propagate all pseudo nodes
if n.Topology == Pseudo {
return report.Nodes{n.ID: n}
}
// Otherwise, if some some reason the container doesn't have a image_id
// (maybe slightly out of sync reports), just drop it
imageID, timestamp, ok := n.Latest.LookupEntry(docker.ImageID)
if !ok {
return report.Nodes{}
}
// Add container id key to the counters, which will later be counted to produce the minor label
id := report.MakeContainerImageNodeID(imageID)
result := NewDerivedNode(id, n).WithTopology(report.ContainerImage)
result.Latest = result.Latest.Set(docker.ImageID, timestamp, imageID)
result.Counters = result.Counters.Add(n.Topology, 1)
return report.Nodes{id: result}
}
// MapContainer2Hostname maps container Nodes to 'hostname' renderabled nodes..
func MapContainer2Hostname(n report.Node, _ report.Networks) report.Nodes {
// Propagate all pseudo nodes
if n.Topology == Pseudo {
return report.Nodes{n.ID: n}
}
// Otherwise, if some some reason the container doesn't have a hostname
// (maybe slightly out of sync reports), just drop it
id, timestamp, ok := n.Latest.LookupEntry(docker.ContainerHostname)
if !ok {
return report.Nodes{}
}
node := NewDerivedNode(id, n).WithTopology(MakeGroupNodeTopology(n.Topology, docker.ContainerHostname))
node.Latest = node.Latest.
Set(docker.ContainerHostname, timestamp, id).
Delete(docker.ContainerName) // TODO(paulbellamy): total hack to render these by hostname instead.
node.Counters = node.Counters.Add(n.Topology, 1)
return report.Nodes{id: node}
}
// ImageNameWithoutVersion splits the image name apart, returning the name
// without the version, if possible
func ImageNameWithoutVersion(name string) string {
parts := strings.SplitN(name, "/", 3)
if len(parts) == 3 {
name = fmt.Sprintf("%s/%s", parts[1], parts[2])
}
parts = strings.SplitN(name, ":", 2)
return parts[0]
}
// MapToEmpty removes all the attributes, children, etc, of a node. Useful when
// we just want to count the presence of nodes.
func MapToEmpty(n report.Node, _ report.Networks) report.Nodes {
return report.Nodes{n.ID: report.MakeNode(n.ID).WithTopology(n.Topology)}
}