container.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. package render
  2. import (
  3. "context"
  4. "regexp"
  5. "strings"
  6. "github.com/weaveworks/scope/report"
  7. )
  8. // Constants are used in the tests.
  9. const (
  10. UncontainedID = "uncontained"
  11. UncontainedMajor = "Uncontained"
  12. // Topology for IPs so we can differentiate them at the end
  13. IP = "IP"
  14. )
  15. // UncontainedIDPrefix is the prefix of uncontained pseudo nodes
  16. var UncontainedIDPrefix = MakePseudoNodeID(UncontainedID, "")
  17. // ContainerRenderer is a Renderer which produces a renderable container
  18. // graph by merging the process graph and the container topology.
  19. // NB We only want processes in container _or_ processes with network connections
  20. // but we need to be careful to ensure we only include each edge once, by only
  21. // including the ProcessRenderer once.
  22. var ContainerRenderer = Memoise(MakeFilter(
  23. func(n report.Node) bool {
  24. // Drop deleted containers
  25. state, ok := n.Latest.Lookup(report.DockerContainerState)
  26. return !ok || state != report.StateDeleted
  27. },
  28. MakeReduce(
  29. MakeMap(
  30. MapProcess2Container,
  31. ProcessRenderer,
  32. ),
  33. ConnectionJoin(MapContainer2IP, report.Container),
  34. ),
  35. ))
  36. const originalNodeID = "original_node_id"
  37. // ConnectionJoin joins the given topology with connections from the
  38. // endpoints topology, using the toIPs function to extract IPs from
  39. // the nodes.
  40. func ConnectionJoin(toIPs func(report.Node) []string, topology string) Renderer {
  41. return connectionJoin{toIPs: toIPs, topology: topology}
  42. }
  43. type connectionJoin struct {
  44. toIPs func(report.Node) []string
  45. topology string
  46. }
  47. func (c connectionJoin) Render(ctx context.Context, rpt report.Report) Nodes {
  48. inputNodes := TopologySelector(c.topology).Render(ctx, rpt).Nodes
  49. // Collect all the IPs we are trying to map to, and which ID they map from
  50. var ipNodes = map[string]string{}
  51. for _, n := range inputNodes {
  52. for _, ip := range c.toIPs(n) {
  53. if _, exists := ipNodes[ip]; exists {
  54. // If an IP is shared between multiple nodes, we can't reliably
  55. // attribute an connection based on its IP
  56. ipNodes[ip] = "" // blank out the mapping so we don't use it
  57. } else {
  58. ipNodes[ip] = n.ID
  59. }
  60. }
  61. }
  62. return MapEndpoints(
  63. func(m report.Node) string {
  64. scope, addr, port, ok := report.ParseEndpointNodeID(m.ID)
  65. if !ok {
  66. return ""
  67. }
  68. id, found := ipNodes[report.MakeScopedEndpointNodeID(scope, addr, "")]
  69. // We also allow for joining on ip:port pairs. This is
  70. // useful for connections to the host IPs which have been
  71. // port mapped to a container can only be unambiguously
  72. // identified with the port.
  73. if !found {
  74. id, found = ipNodes[report.MakeScopedEndpointNodeID(scope, addr, port)]
  75. }
  76. if !found || id == "" {
  77. return ""
  78. }
  79. // Not an IP we blanked out earlier.
  80. //
  81. // MapEndpoints is guaranteed to find a node with this id
  82. // (and hence not have to create one), since we got the id
  83. // from ipNodes, which is populated from c.topology, which
  84. // is where MapEndpoints will look.
  85. return id
  86. }, c.topology).Render(ctx, rpt)
  87. }
  88. // FilterEmpty is a Renderer which filters out nodes which have no children
  89. // from the specified topology.
  90. func FilterEmpty(topology string, r Renderer) Renderer {
  91. return MakeFilter(HasChildren(topology), r)
  92. }
  93. // HasChildren returns true if the node has no children from the specified
  94. // topology.
  95. func HasChildren(topology string) FilterFunc {
  96. return func(n report.Node) bool {
  97. count := 0
  98. n.Children.ForEach(func(child report.Node) {
  99. if child.Topology == topology {
  100. count++
  101. }
  102. })
  103. return count > 0
  104. }
  105. }
  106. type containerWithImageNameRenderer struct{}
  107. // Render produces a container graph where the the latest metadata contains the
  108. // container image name, if found.
  109. func (r containerWithImageNameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
  110. containers := ContainerRenderer.Render(ctx, rpt)
  111. images := SelectContainerImage.Render(ctx, rpt)
  112. outputs := make(report.Nodes, len(containers.Nodes))
  113. for id, c := range containers.Nodes {
  114. outputs[id] = c
  115. imageID, ok := c.Latest.Lookup(report.DockerImageID)
  116. if !ok {
  117. continue
  118. }
  119. image, ok := images.Nodes[report.MakeContainerImageNodeID(imageID)]
  120. if !ok {
  121. continue
  122. }
  123. imageNodeID := containerImageNodeID(image)
  124. if imageNodeID == "" {
  125. continue
  126. }
  127. c.Latest = c.Latest.Propagate(image.Latest, report.DockerImageName, report.DockerImageTag,
  128. report.DockerImageSize, report.DockerImageVirtualSize, report.DockerImageLabelPrefix+"works.weave.role")
  129. c.Parents = c.Parents.
  130. Delete(report.ContainerImage).
  131. AddString(report.ContainerImage, imageNodeID)
  132. outputs[id] = c
  133. }
  134. return Nodes{Nodes: outputs, Filtered: containers.Filtered}
  135. }
  136. // ContainerWithImageNameRenderer is a Renderer which produces a container
  137. // graph where the ranks are the image names, not their IDs
  138. var ContainerWithImageNameRenderer = Memoise(containerWithImageNameRenderer{})
  139. // ContainerImageRenderer produces a graph where each node is a container image
  140. // with the original containers as children
  141. var ContainerImageRenderer = Memoise(FilterEmpty(report.Container,
  142. MakeMap(
  143. MapContainerImage2Name,
  144. containerImageRenderer{},
  145. ),
  146. ))
  147. // ContainerHostnameRenderer is a Renderer which produces a renderable container
  148. // by hostname graph..
  149. //
  150. // not memoised
  151. var ContainerHostnameRenderer = FilterEmpty(report.Container,
  152. containerHostnameRenderer{},
  153. )
  154. 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`)
  155. // MapContainer2IP maps container nodes to their IP addresses (outputs
  156. // multiple nodes). This allows container to be joined directly with
  157. // the endpoint topology.
  158. func MapContainer2IP(m report.Node) []string {
  159. // if this container doesn't make connections, we can ignore it
  160. _, doesntMakeConnections := m.Latest.Lookup(report.DoesNotMakeConnections)
  161. // if this container belongs to the host's networking namespace
  162. // we cannot use its IP to attribute connections
  163. // (they could come from any other process on the host or DNAT-ed IPs)
  164. _, isInHostNetwork := m.Latest.Lookup(report.DockerIsInHostNetwork)
  165. if doesntMakeConnections || isInHostNetwork {
  166. return nil
  167. }
  168. result := []string{}
  169. if addrs, ok := m.Sets.Lookup(report.DockerContainerIPsWithScopes); ok {
  170. for _, addr := range addrs {
  171. scope, addr, ok := report.ParseAddressNodeID(addr)
  172. if !ok {
  173. continue
  174. }
  175. // loopback addresses are shared among all namespaces
  176. // so we can't use them to attribute connections to a container
  177. if report.IsLoopback(addr) {
  178. continue
  179. }
  180. id := report.MakeScopedEndpointNodeID(scope, addr, "")
  181. result = append(result, id)
  182. }
  183. }
  184. // Also output all the host:port port mappings (see above comment).
  185. // In this case we assume this doesn't need a scope, as they are for host IPs.
  186. ports, _ := m.Sets.Lookup(report.DockerContainerPorts)
  187. for _, portMapping := range ports {
  188. if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
  189. ip, port := mapping[1], mapping[2]
  190. id := report.MakeScopedEndpointNodeID("", ip, port)
  191. result = append(result, id)
  192. }
  193. }
  194. return result
  195. }
  196. // MapProcess2Container maps process Nodes to container
  197. // Nodes.
  198. //
  199. // Pseudo nodes are passed straight through.
  200. //
  201. // If this function is given a node without a docker_container_id, it
  202. // will produce an "Uncontained" pseudo node.
  203. //
  204. // Otherwise, this function will produce a node with the correct ID
  205. // format for a container, but without any Major or Minor labels.
  206. // It does not have enough info to do that, and the resulting graph
  207. // must be merged with a container graph to get that info.
  208. func MapProcess2Container(n report.Node) report.Node {
  209. // Propagate pseudo nodes
  210. if n.Topology == Pseudo {
  211. return n
  212. }
  213. // Otherwise, if the process is not in a container, group it into
  214. // a per-host "Uncontained" node.
  215. var (
  216. id string
  217. node report.Node
  218. )
  219. if containerID, ok := n.Latest.Lookup(report.DockerContainerID); ok {
  220. id = report.MakeContainerNodeID(containerID)
  221. node = NewDerivedNode(id, n).WithTopology(report.Container)
  222. } else {
  223. hostID, _, _ := report.ParseProcessNodeID(n.ID)
  224. id = MakePseudoNodeID(UncontainedID, hostID)
  225. node = NewDerivedPseudoNode(id, n)
  226. }
  227. return node
  228. }
  229. // containerImageRenderer produces a graph where each node is a container image
  230. // with the original containers as children
  231. type containerImageRenderer struct{}
  232. func (m containerImageRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
  233. containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
  234. ret := newJoinResults(rpt.ContainerImage.Nodes)
  235. for _, n := range containers.Nodes {
  236. if n.Topology == Pseudo {
  237. ret.passThrough(n)
  238. continue
  239. }
  240. // If some some reason the container doesn't have a image_id, just drop it
  241. imageID, ok := n.Latest.Lookup(report.DockerImageID)
  242. if !ok {
  243. continue
  244. }
  245. id := report.MakeContainerImageNodeID(imageID)
  246. ret.addChildAndChildren(n, id, report.ContainerImage)
  247. }
  248. return ret.result(ctx, containers)
  249. }
  250. func containerImageNodeID(n report.Node) string {
  251. imageName, ok := n.Latest.Lookup(report.DockerImageName)
  252. if !ok {
  253. return ""
  254. }
  255. parts := strings.SplitN(imageName, "/", 3)
  256. if len(parts) == 3 {
  257. imageName = strings.Join(parts[1:3], "/")
  258. }
  259. imageNameWithoutTag := strings.SplitN(imageName, ":", 2)[0]
  260. return report.MakeContainerImageNodeID(imageNameWithoutTag)
  261. }
  262. // MapContainerImage2Name ignores image versions
  263. func MapContainerImage2Name(n report.Node) report.Node {
  264. // Propagate all pseudo nodes
  265. if n.Topology == Pseudo {
  266. return n
  267. }
  268. n.ID = containerImageNodeID(n)
  269. if n.ID == "" {
  270. return report.Node{}
  271. }
  272. return n
  273. }
  274. var containerHostnameTopology = MakeGroupNodeTopology(report.Container, report.DockerContainerHostname)
  275. // containerHostnameRenderer collects containers by docker hostname
  276. type containerHostnameRenderer struct{}
  277. func (m containerHostnameRenderer) Render(ctx context.Context, rpt report.Report) Nodes {
  278. containers := ContainerWithImageNameRenderer.Render(ctx, rpt)
  279. ret := newJoinResults(nil)
  280. for _, n := range containers.Nodes {
  281. if n.Topology == Pseudo {
  282. ret.passThrough(n)
  283. continue
  284. }
  285. // If some some reason the container doesn't have a hostname, just drop it
  286. id, ok := n.Latest.Lookup(report.DockerContainerHostname)
  287. if !ok {
  288. continue
  289. }
  290. ret.addChildAndChildren(n, id, containerHostnameTopology)
  291. }
  292. return ret.result(ctx, containers)
  293. }