123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- package render
- import (
- "context"
- "regexp"
- "strings"
- "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"
- )
- // UncontainedIDPrefix is the prefix of uncontained pseudo nodes
- var UncontainedIDPrefix = MakePseudoNodeID(UncontainedID, "")
- // 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 = Memoise(MakeFilter(
- func(n report.Node) bool {
- // Drop deleted containers
- state, ok := n.Latest.Lookup(report.DockerContainerState)
- return !ok || state != report.StateDeleted
- },
- MakeReduce(
- MakeMap(
- MapProcess2Container,
- ProcessRenderer,
- ),
- ConnectionJoin(MapContainer2IP, report.Container),
- ),
- ))
- const originalNodeID = "original_node_id"
- // ConnectionJoin joins the given topology with connections from the
- // endpoints topology, using the toIPs function to extract IPs from
- // the nodes.
- func ConnectionJoin(toIPs func(report.Node) []string, topology string) Renderer {
- return connectionJoin{toIPs: toIPs, topology: topology}
- }
- type connectionJoin struct {
- toIPs func(report.Node) []string
- topology string
- }
- func (c connectionJoin) Render(ctx context.Context, rpt report.Report) Nodes {
- inputNodes := TopologySelector(c.topology).Render(ctx, rpt).Nodes
- // Collect all the IPs we are trying to map to, and which ID they map from
- var ipNodes = map[string]string{}
- for _, n := range inputNodes {
- for _, ip := range c.toIPs(n) {
- if _, exists := ipNodes[ip]; exists {
- // If an IP is shared between multiple nodes, we can't reliably
- // attribute an connection based on its IP
- ipNodes[ip] = "" // blank out the mapping so we don't use it
- } else {
- ipNodes[ip] = n.ID
- }
- }
- }
- return MapEndpoints(
- func(m report.Node) string {
- scope, addr, port, ok := report.ParseEndpointNodeID(m.ID)
- if !ok {
- return ""
- }
- id, found := ipNodes[report.MakeScopedEndpointNodeID(scope, addr, "")]
- // We also allow for joining on ip:port pairs. This is
- // useful for connections to the host IPs which have been
- // port mapped to a container can only be unambiguously
- // identified with the port.
- if !found {
- id, found = ipNodes[report.MakeScopedEndpointNodeID(scope, addr, port)]
- }
- if !found || id == "" {
- return ""
- }
- // Not an IP we blanked out earlier.
- //
- // MapEndpoints is guaranteed to find a node with this id
- // (and hence not have to create one), since we got the id
- // from ipNodes, which is populated from c.topology, which
- // is where MapEndpoints will look.
- return id
- }, c.topology).Render(ctx, rpt)
- }
- // FilterEmpty is a Renderer which filters out nodes which have no children
- // from the specified topology.
- func FilterEmpty(topology string, r Renderer) Renderer {
- return MakeFilter(HasChildren(topology), r)
- }
- // HasChildren returns true if the node has no children from the specified
- // topology.
- func HasChildren(topology string) FilterFunc {
- return func(n report.Node) bool {
- count := 0
- n.Children.ForEach(func(child report.Node) {
- if child.Topology == topology {
- count++
- }
- })
- return count > 0
- }
- }
- type containerWithImageNameRenderer struct{}
- // Render produces a container graph where the the latest metadata contains the
- // container image name, if found.
- func (r containerWithImageNameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
- containers := ContainerRenderer.Render(ctx, rpt)
- images := SelectContainerImage.Render(ctx, rpt)
- outputs := make(report.Nodes, len(containers.Nodes))
- for id, c := range containers.Nodes {
- outputs[id] = c
- imageID, ok := c.Latest.Lookup(report.DockerImageID)
- if !ok {
- continue
- }
- image, ok := images.Nodes[report.MakeContainerImageNodeID(imageID)]
- if !ok {
- continue
- }
- imageNodeID := containerImageNodeID(image)
- if imageNodeID == "" {
- continue
- }
- c.Latest = c.Latest.Propagate(image.Latest, report.DockerImageName, report.DockerImageTag,
- report.DockerImageSize, report.DockerImageVirtualSize, report.DockerImageLabelPrefix+"works.weave.role")
- c.Parents = c.Parents.
- Delete(report.ContainerImage).
- AddString(report.ContainerImage, imageNodeID)
- outputs[id] = c
- }
- return Nodes{Nodes: outputs, Filtered: containers.Filtered}
- }
- // ContainerWithImageNameRenderer is a Renderer which produces a container
- // graph where the ranks are the image names, not their IDs
- var ContainerWithImageNameRenderer = Memoise(containerWithImageNameRenderer{})
- // ContainerImageRenderer produces a graph where each node is a container image
- // with the original containers as children
- var ContainerImageRenderer = Memoise(FilterEmpty(report.Container,
- MakeMap(
- MapContainerImage2Name,
- containerImageRenderer{},
- ),
- ))
- // ContainerHostnameRenderer is a Renderer which produces a renderable container
- // by hostname graph..
- //
- // not memoised
- var ContainerHostnameRenderer = FilterEmpty(report.Container,
- containerHostnameRenderer{},
- )
- 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`)
- // 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) []string {
- // if this container doesn't make connections, we can ignore it
- _, doesntMakeConnections := m.Latest.Lookup(report.DoesNotMakeConnections)
- // if this container belongs to the host's networking namespace
- // we cannot use its IP to attribute connections
- // (they could come from any other process on the host or DNAT-ed IPs)
- _, isInHostNetwork := m.Latest.Lookup(report.DockerIsInHostNetwork)
- if doesntMakeConnections || isInHostNetwork {
- return nil
- }
- result := []string{}
- if addrs, ok := m.Sets.Lookup(report.DockerContainerIPsWithScopes); ok {
- for _, addr := range addrs {
- scope, addr, ok := report.ParseAddressNodeID(addr)
- if !ok {
- continue
- }
- // loopback addresses are shared among all namespaces
- // so we can't use them to attribute connections to a container
- if report.IsLoopback(addr) {
- continue
- }
- id := report.MakeScopedEndpointNodeID(scope, addr, "")
- result = append(result, id)
- }
- }
- // 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(report.DockerContainerPorts)
- for _, portMapping := range ports {
- if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
- ip, port := mapping[1], mapping[2]
- id := report.MakeScopedEndpointNodeID("", ip, port)
- result = append(result, id)
- }
- }
- return result
- }
- // MapProcess2Container maps process Nodes to container
- // Nodes.
- //
- // Pseudo nodes are passed straight through.
- //
- // If this function is given a node without a docker_container_id, 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.Node {
- // Propagate pseudo nodes
- if n.Topology == Pseudo {
- return n
- }
- // Otherwise, if the process is not in a container, group it into
- // a per-host "Uncontained" node.
- var (
- id string
- node report.Node
- )
- if containerID, ok := n.Latest.Lookup(report.DockerContainerID); ok {
- id = report.MakeContainerNodeID(containerID)
- node = NewDerivedNode(id, n).WithTopology(report.Container)
- } else {
- hostID, _, _ := report.ParseProcessNodeID(n.ID)
- id = MakePseudoNodeID(UncontainedID, hostID)
- node = NewDerivedPseudoNode(id, n)
- }
- return node
- }
- // containerImageRenderer produces a graph where each node is a container image
- // with the original containers as children
- type containerImageRenderer struct{}
- func (m containerImageRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
- containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
- ret := newJoinResults(rpt.ContainerImage.Nodes)
- for _, n := range containers.Nodes {
- if n.Topology == Pseudo {
- ret.passThrough(n)
- continue
- }
- // If some some reason the container doesn't have a image_id, just drop it
- imageID, ok := n.Latest.Lookup(report.DockerImageID)
- if !ok {
- continue
- }
- id := report.MakeContainerImageNodeID(imageID)
- ret.addChildAndChildren(n, id, report.ContainerImage)
- }
- return ret.result(ctx, containers)
- }
- func containerImageNodeID(n report.Node) string {
- imageName, ok := n.Latest.Lookup(report.DockerImageName)
- if !ok {
- return ""
- }
- parts := strings.SplitN(imageName, "/", 3)
- if len(parts) == 3 {
- imageName = strings.Join(parts[1:3], "/")
- }
- imageNameWithoutTag := strings.SplitN(imageName, ":", 2)[0]
- return report.MakeContainerImageNodeID(imageNameWithoutTag)
- }
- // MapContainerImage2Name ignores image versions
- func MapContainerImage2Name(n report.Node) report.Node {
- // Propagate all pseudo nodes
- if n.Topology == Pseudo {
- return n
- }
- n.ID = containerImageNodeID(n)
- if n.ID == "" {
- return report.Node{}
- }
- return n
- }
- var containerHostnameTopology = MakeGroupNodeTopology(report.Container, report.DockerContainerHostname)
- // containerHostnameRenderer collects containers by docker hostname
- type containerHostnameRenderer struct{}
- func (m containerHostnameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
- containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
- ret := newJoinResults(nil)
- for _, n := range containers.Nodes {
- if n.Topology == Pseudo {
- ret.passThrough(n)
- continue
- }
- // If some some reason the container doesn't have a hostname, just drop it
- id, ok := n.Latest.Lookup(report.DockerContainerHostname)
- if !ok {
- continue
- }
- ret.addChildAndChildren(n, id, containerHostnameTopology)
- }
- return ret.result(ctx, containers)
- }
|