api_topologies.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "sort"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/gorilla/mux"
  12. opentracing "github.com/opentracing/opentracing-go"
  13. log "github.com/sirupsen/logrus"
  14. "github.com/weaveworks/scope/probe/docker"
  15. "github.com/weaveworks/scope/probe/kubernetes"
  16. "github.com/weaveworks/scope/render"
  17. "github.com/weaveworks/scope/report"
  18. )
  19. const (
  20. apiTopologyURL = "/api/topology/"
  21. processesID = "processes"
  22. processesByNameID = "processes-by-name"
  23. systemGroupID = "system"
  24. containersID = "containers"
  25. containersByHostnameID = "containers-by-hostname"
  26. containersByImageID = "containers-by-image"
  27. podsID = "pods"
  28. kubeControllersID = "kube-controllers"
  29. servicesID = "services"
  30. hostsID = "hosts"
  31. weaveID = "weave"
  32. ecsTasksID = "ecs-tasks"
  33. ecsServicesID = "ecs-services"
  34. swarmServicesID = "swarm-services"
  35. )
  36. var (
  37. topologyRegistry = MakeRegistry()
  38. unmanagedFilter = APITopologyOptionGroup{
  39. ID: "pseudo",
  40. Default: "hide",
  41. Options: []APITopologyOption{
  42. {Value: "show", Label: "Show unmanaged", filter: nil, filterPseudo: false},
  43. {Value: "hide", Label: "Hide unmanaged", filter: render.IsNotPseudo, filterPseudo: true},
  44. },
  45. }
  46. storageFilter = APITopologyOptionGroup{
  47. ID: "storage",
  48. Default: "hide",
  49. Options: []APITopologyOption{
  50. {Value: "show", Label: "Show storage", filter: nil, filterPseudo: false},
  51. {Value: "hide", Label: "Hide storage", filter: render.IsPodComponent, filterPseudo: false},
  52. },
  53. }
  54. snapshotFilter = APITopologyOptionGroup{
  55. ID: "snapshot",
  56. Default: "hide",
  57. Options: []APITopologyOption{
  58. {Value: "show", Label: "Show snapshots", filter: nil, filterPseudo: false},
  59. {Value: "hide", Label: "Hide snapshots", filter: render.IsNonSnapshotComponent, filterPseudo: false},
  60. },
  61. }
  62. )
  63. // namespaceFilters generates a namespace selector option group based on the given namespaces
  64. func namespaceFilters(namespaces []string, noneLabel string) APITopologyOptionGroup {
  65. options := APITopologyOptionGroup{ID: "namespace", Default: "", SelectType: "union", NoneLabel: noneLabel}
  66. for _, namespace := range namespaces {
  67. options.Options = append(options.Options, APITopologyOption{
  68. Value: namespace, Label: namespace, filter: render.IsNamespace(namespace), filterPseudo: false,
  69. })
  70. }
  71. return options
  72. }
  73. // updateFilters updates the available filters based on the current report.
  74. func updateFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
  75. topologies = updateKubeFilters(rpt, topologies)
  76. topologies = updateSwarmFilters(rpt, topologies)
  77. return topologies
  78. }
  79. func updateSwarmFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
  80. namespaces := map[string]struct{}{}
  81. for _, n := range rpt.SwarmService.Nodes {
  82. if namespace, ok := n.Latest.Lookup(docker.StackNamespace); ok {
  83. namespaces[namespace] = struct{}{}
  84. }
  85. }
  86. if len(namespaces) == 0 {
  87. // We only want to apply filters when we have swarm-related nodes,
  88. // so if we don't then return early
  89. return topologies
  90. }
  91. ns := []string{}
  92. for namespace := range namespaces {
  93. ns = append(ns, namespace)
  94. }
  95. topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely
  96. for i, t := range topologies {
  97. if t.id == containersID || t.id == swarmServicesID {
  98. topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{
  99. namespaceFilters(ns, "All Stacks"),
  100. })
  101. }
  102. }
  103. return topologies
  104. }
  105. func updateKubeFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
  106. ns := []string{}
  107. for _, n := range rpt.Namespace.Nodes {
  108. name, ok := n.Latest.Lookup(kubernetes.Name)
  109. if !ok {
  110. continue
  111. }
  112. ns = append(ns, name)
  113. }
  114. if len(ns) == 0 {
  115. return topologies
  116. }
  117. sort.Strings(ns)
  118. topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely
  119. for i, t := range topologies {
  120. if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == kubeControllersID {
  121. topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{
  122. namespaceFilters(ns, "All Namespaces"),
  123. })
  124. }
  125. }
  126. return topologies
  127. }
  128. // mergeTopologyFilters recursively merges in new options on a topology description
  129. func mergeTopologyFilters(t APITopologyDesc, options []APITopologyOptionGroup) APITopologyDesc {
  130. t.Options = append(append([]APITopologyOptionGroup{}, t.Options...), options...)
  131. newSubTopologies := make([]APITopologyDesc, len(t.SubTopologies))
  132. for i, sub := range t.SubTopologies {
  133. newSubTopologies[i] = mergeTopologyFilters(sub, options)
  134. }
  135. t.SubTopologies = newSubTopologies
  136. return t
  137. }
  138. // MakeAPITopologyOption provides an external interface to the package for creating an APITopologyOption.
  139. func MakeAPITopologyOption(value string, label string, filterFunc render.FilterFunc, pseudo bool) APITopologyOption {
  140. return APITopologyOption{Value: value, Label: label, filter: filterFunc, filterPseudo: pseudo}
  141. }
  142. // Registry is a threadsafe store of the available topologies
  143. type Registry struct {
  144. sync.RWMutex
  145. items map[string]APITopologyDesc
  146. }
  147. // MakeRegistry returns a new Registry
  148. func MakeRegistry() *Registry {
  149. registry := &Registry{
  150. items: map[string]APITopologyDesc{},
  151. }
  152. containerFilters := []APITopologyOptionGroup{
  153. {
  154. ID: systemGroupID,
  155. Default: "application",
  156. Options: []APITopologyOption{
  157. {Value: "all", Label: "All", filter: nil, filterPseudo: false},
  158. {Value: "system", Label: "System containers", filter: render.IsSystem, filterPseudo: false},
  159. {Value: "application", Label: "Application containers", filter: render.IsApplication, filterPseudo: false}},
  160. },
  161. {
  162. ID: "stopped",
  163. Default: "running",
  164. Options: []APITopologyOption{
  165. {Value: "stopped", Label: "Stopped containers", filter: render.IsStopped, filterPseudo: false},
  166. {Value: "running", Label: "Running containers", filter: render.IsRunning, filterPseudo: false},
  167. {Value: "both", Label: "Both", filter: nil, filterPseudo: false},
  168. },
  169. },
  170. {
  171. ID: "pseudo",
  172. Default: "hide",
  173. Options: []APITopologyOption{
  174. {Value: "show", Label: "Show uncontained", filter: nil, filterPseudo: false},
  175. {Value: "hide", Label: "Hide uncontained", filter: render.IsNotPseudo, filterPseudo: true},
  176. },
  177. },
  178. }
  179. unconnectedFilter := []APITopologyOptionGroup{
  180. {
  181. ID: "unconnected",
  182. Default: "hide",
  183. Options: []APITopologyOption{
  184. {Value: "show", Label: "Show unconnected", filter: nil, filterPseudo: false},
  185. {Value: "hide", Label: "Hide unconnected", filter: render.IsConnected, filterPseudo: false},
  186. },
  187. },
  188. }
  189. // Topology option labels should tell the current state. The first item must
  190. // be the verb to get to that state
  191. registry.Add(
  192. APITopologyDesc{
  193. id: processesID,
  194. renderer: render.ConnectedProcessRenderer,
  195. Name: "Processes",
  196. Rank: 1,
  197. Options: unconnectedFilter,
  198. HideIfEmpty: true,
  199. },
  200. APITopologyDesc{
  201. id: processesByNameID,
  202. parent: processesID,
  203. renderer: render.ProcessNameRenderer,
  204. Name: "by name",
  205. Options: unconnectedFilter,
  206. HideIfEmpty: true,
  207. },
  208. APITopologyDesc{
  209. id: containersID,
  210. renderer: render.ContainerWithImageNameRenderer,
  211. Name: "Containers",
  212. Rank: 2,
  213. Options: containerFilters,
  214. },
  215. APITopologyDesc{
  216. id: containersByHostnameID,
  217. parent: containersID,
  218. renderer: render.ContainerHostnameRenderer,
  219. Name: "by DNS name",
  220. Options: containerFilters,
  221. },
  222. APITopologyDesc{
  223. id: containersByImageID,
  224. parent: containersID,
  225. renderer: render.ContainerImageRenderer,
  226. Name: "by image",
  227. Options: containerFilters,
  228. },
  229. APITopologyDesc{
  230. id: podsID,
  231. renderer: render.PodRenderer,
  232. Name: "Pods",
  233. Rank: 3,
  234. Options: []APITopologyOptionGroup{snapshotFilter, storageFilter, unmanagedFilter},
  235. HideIfEmpty: true,
  236. },
  237. APITopologyDesc{
  238. id: kubeControllersID,
  239. parent: podsID,
  240. renderer: render.KubeControllerRenderer,
  241. Name: "Controllers",
  242. Options: []APITopologyOptionGroup{unmanagedFilter},
  243. HideIfEmpty: true,
  244. },
  245. APITopologyDesc{
  246. id: servicesID,
  247. parent: podsID,
  248. renderer: render.PodServiceRenderer,
  249. Name: "Services",
  250. Options: []APITopologyOptionGroup{unmanagedFilter},
  251. HideIfEmpty: true,
  252. },
  253. APITopologyDesc{
  254. id: ecsTasksID,
  255. renderer: render.ECSTaskRenderer,
  256. Name: "Tasks",
  257. Rank: 3,
  258. Options: []APITopologyOptionGroup{unmanagedFilter},
  259. HideIfEmpty: true,
  260. },
  261. APITopologyDesc{
  262. id: ecsServicesID,
  263. parent: ecsTasksID,
  264. renderer: render.ECSServiceRenderer,
  265. Name: "Services",
  266. Options: []APITopologyOptionGroup{unmanagedFilter},
  267. HideIfEmpty: true,
  268. },
  269. APITopologyDesc{
  270. id: swarmServicesID,
  271. renderer: render.SwarmServiceRenderer,
  272. Name: "Services",
  273. Rank: 3,
  274. Options: []APITopologyOptionGroup{unmanagedFilter},
  275. HideIfEmpty: true,
  276. },
  277. APITopologyDesc{
  278. id: hostsID,
  279. renderer: render.HostRenderer,
  280. Name: "Hosts",
  281. Rank: 4,
  282. },
  283. APITopologyDesc{
  284. id: weaveID,
  285. parent: hostsID,
  286. renderer: render.WeaveRenderer,
  287. Name: "Weave Net",
  288. },
  289. )
  290. return registry
  291. }
  292. // APITopologyDesc is returned in a list by the /api/topology handler.
  293. type APITopologyDesc struct {
  294. id string
  295. parent string
  296. renderer render.Renderer
  297. Name string `json:"name"`
  298. Rank int `json:"rank"`
  299. HideIfEmpty bool `json:"hide_if_empty"`
  300. Options []APITopologyOptionGroup `json:"options"`
  301. URL string `json:"url"`
  302. SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"`
  303. Stats topologyStats `json:"stats,omitempty"`
  304. }
  305. type byName []APITopologyDesc
  306. func (a byName) Len() int { return len(a) }
  307. func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  308. func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name }
  309. // APITopologyOptionGroup describes a group of APITopologyOptions
  310. type APITopologyOptionGroup struct {
  311. ID string `json:"id"`
  312. // Default value for the option. Used if the value is omitted; not used if the value is ""
  313. Default string `json:"defaultValue"`
  314. Options []APITopologyOption `json:"options,omitempty"`
  315. // SelectType describes how options can be picked. Currently defined values:
  316. // "one": Default if empty. Exactly one option may be picked from the list.
  317. // "union": Any number of options may be picked. Nodes matching any option filter selected are displayed.
  318. // Value and Default should be a ","-separated list.
  319. SelectType string `json:"selectType,omitempty"`
  320. // For "union" type, this is the label the UI should use to represent the case where nothing is selected
  321. NoneLabel string `json:"noneLabel,omitempty"`
  322. }
  323. // Get the render filters to use for this option group, if any, or nil otherwise.
  324. func (g APITopologyOptionGroup) filter(value string) render.FilterFunc {
  325. var values []string
  326. switch g.SelectType {
  327. case "", "one":
  328. values = []string{value}
  329. case "union":
  330. values = strings.Split(value, ",")
  331. default:
  332. log.Errorf("Invalid select type %s for option group %s, ignoring option", g.SelectType, g.ID)
  333. return nil
  334. }
  335. filters := []render.FilterFunc{}
  336. for _, opt := range g.Options {
  337. for _, v := range values {
  338. if v != opt.Value {
  339. continue
  340. }
  341. var filter render.FilterFunc
  342. if opt.filter == nil {
  343. // No filter means match everything (pseudo doesn't matter)
  344. filter = func(n report.Node) bool { return true }
  345. } else if opt.filterPseudo {
  346. // Apply filter to pseudo topologies also
  347. filter = opt.filter
  348. } else {
  349. // Allow all pseudo topology nodes, only apply filter to non-pseudo
  350. filter = render.AnyFilterFunc(render.IsPseudoTopology, opt.filter)
  351. }
  352. filters = append(filters, filter)
  353. }
  354. }
  355. if len(filters) == 0 {
  356. return nil
  357. }
  358. return render.AnyFilterFunc(filters...)
  359. }
  360. // APITopologyOption describes a &param=value to a given topology.
  361. type APITopologyOption struct {
  362. Value string `json:"value"`
  363. Label string `json:"label"`
  364. filter render.FilterFunc
  365. filterPseudo bool
  366. }
  367. type topologyStats struct {
  368. NodeCount int `json:"node_count"`
  369. NonpseudoNodeCount int `json:"nonpseudo_node_count"`
  370. EdgeCount int `json:"edge_count"`
  371. FilteredNodes int `json:"filtered_nodes"`
  372. }
  373. // deserializeTimestamp converts the ISO8601 query param into a proper timestamp.
  374. func deserializeTimestamp(timestamp string) time.Time {
  375. if timestamp != "" {
  376. result, err := time.Parse(time.RFC3339, timestamp)
  377. if err != nil {
  378. log.Errorf("Error parsing timestamp '%s' - make sure the time format is correct", timestamp)
  379. }
  380. return result
  381. }
  382. // Default to current time if no timestamp is provided.
  383. return time.Now()
  384. }
  385. // AddContainerFilters adds to the default Registry (topologyRegistry)'s containerFilters
  386. func AddContainerFilters(newFilters ...APITopologyOption) {
  387. topologyRegistry.AddContainerFilters(newFilters...)
  388. }
  389. // AddContainerFilters adds container filters to this Registry
  390. func (r *Registry) AddContainerFilters(newFilters ...APITopologyOption) {
  391. r.Lock()
  392. defer r.Unlock()
  393. for _, key := range []string{containersID, containersByHostnameID, containersByImageID} {
  394. for i := range r.items[key].Options {
  395. if r.items[key].Options[i].ID == systemGroupID {
  396. r.items[key].Options[i].Options = append(r.items[key].Options[i].Options, newFilters...)
  397. break
  398. }
  399. }
  400. }
  401. }
  402. // Add inserts a topologyDesc to the Registry's items map
  403. func (r *Registry) Add(ts ...APITopologyDesc) {
  404. r.Lock()
  405. defer r.Unlock()
  406. for _, t := range ts {
  407. t.URL = apiTopologyURL + t.id
  408. t.renderer = render.Memoise(t.renderer)
  409. if t.parent != "" {
  410. parent := r.items[t.parent]
  411. parent.SubTopologies = append(parent.SubTopologies, t)
  412. r.items[t.parent] = parent
  413. }
  414. r.items[t.id] = t
  415. }
  416. }
  417. func (r *Registry) get(name string) (APITopologyDesc, bool) {
  418. r.RLock()
  419. defer r.RUnlock()
  420. t, ok := r.items[name]
  421. return t, ok
  422. }
  423. func (r *Registry) walk(f func(APITopologyDesc)) {
  424. r.RLock()
  425. defer r.RUnlock()
  426. descs := []APITopologyDesc{}
  427. for _, desc := range r.items {
  428. if desc.parent != "" {
  429. continue
  430. }
  431. descs = append(descs, desc)
  432. }
  433. sort.Sort(byName(descs))
  434. for _, desc := range descs {
  435. f(desc)
  436. }
  437. }
  438. // makeTopologyList returns a handler that yields an APITopologyList.
  439. func (r *Registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
  440. return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  441. timestamp := deserializeTimestamp(req.URL.Query().Get("timestamp"))
  442. report, err := rep.Report(ctx, timestamp)
  443. if err != nil {
  444. respondWith(ctx, w, http.StatusInternalServerError, err)
  445. return
  446. }
  447. report.UnsafeRemovePartMergedNodes(ctx)
  448. respondWith(ctx, w, http.StatusOK, r.renderTopologies(ctx, report, req))
  449. }
  450. }
  451. func (r *Registry) renderTopologies(ctx context.Context, rpt report.Report, req *http.Request) []APITopologyDesc {
  452. span, ctx := opentracing.StartSpanFromContext(ctx, "app.renderTopologies")
  453. defer span.Finish()
  454. topologies := []APITopologyDesc{}
  455. req.ParseForm()
  456. r.walk(func(desc APITopologyDesc) {
  457. if ctx.Err() != nil {
  458. return
  459. }
  460. renderer, filter, _ := r.RendererForTopology(desc.id, req.Form, rpt)
  461. desc.Stats = computeStats(ctx, rpt, renderer, filter)
  462. for i, sub := range desc.SubTopologies {
  463. renderer, filter, _ := r.RendererForTopology(sub.id, req.Form, rpt)
  464. desc.SubTopologies[i].Stats = computeStats(ctx, rpt, renderer, filter)
  465. }
  466. topologies = append(topologies, desc)
  467. })
  468. return updateFilters(rpt, topologies)
  469. }
  470. func computeStats(ctx context.Context, rpt report.Report, renderer render.Renderer, transformer render.Transformer) topologyStats {
  471. span, ctx := opentracing.StartSpanFromContext(ctx, "app.computeStats")
  472. defer span.Finish()
  473. var (
  474. nodes int
  475. realNodes int
  476. edges int
  477. )
  478. r := render.Render(ctx, rpt, renderer, transformer)
  479. for _, n := range r.Nodes {
  480. nodes++
  481. if n.Topology != render.Pseudo {
  482. realNodes++
  483. }
  484. edges += len(n.Adjacency)
  485. }
  486. return topologyStats{
  487. NodeCount: nodes,
  488. NonpseudoNodeCount: realNodes,
  489. EdgeCount: edges,
  490. FilteredNodes: r.Filtered,
  491. }
  492. }
  493. // RendererForTopology ..
  494. func (r *Registry) RendererForTopology(topologyID string, values url.Values, rpt report.Report) (render.Renderer, render.Transformer, error) {
  495. topology, ok := r.get(topologyID)
  496. if !ok {
  497. return nil, nil, fmt.Errorf("topology not found: %s", topologyID)
  498. }
  499. topology = updateFilters(rpt, []APITopologyDesc{topology})[0]
  500. if len(values) == 0 {
  501. // if no options where provided, only apply base filter
  502. return topology.renderer, render.FilterUnconnectedPseudo, nil
  503. }
  504. var filters []render.FilterFunc
  505. for _, group := range topology.Options {
  506. value := group.Default
  507. if vs := values[group.ID]; len(vs) > 0 {
  508. value = vs[0]
  509. }
  510. if filter := group.filter(value); filter != nil {
  511. filters = append(filters, filter)
  512. }
  513. }
  514. if len(filters) > 0 {
  515. return topology.renderer, render.Transformers([]render.Transformer{render.ComposeFilterFuncs(filters...), render.FilterUnconnectedPseudo}), nil
  516. }
  517. return topology.renderer, render.FilterUnconnectedPseudo, nil
  518. }
  519. type reporterHandler func(context.Context, Reporter, http.ResponseWriter, *http.Request)
  520. func captureReporter(rep Reporter, f reporterHandler) CtxHandlerFunc {
  521. return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
  522. f(ctx, rep, w, r)
  523. }
  524. }
  525. func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFunc {
  526. return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  527. var (
  528. topologyID = mux.Vars(req)["topology"]
  529. timestamp = deserializeTimestamp(req.URL.Query().Get("timestamp"))
  530. )
  531. if _, ok := r.get(topologyID); !ok {
  532. http.NotFound(w, req)
  533. return
  534. }
  535. rpt, err := rep.Report(ctx, timestamp)
  536. if err != nil {
  537. respondWith(ctx, w, http.StatusInternalServerError, err)
  538. return
  539. }
  540. rpt.UnsafeRemovePartMergedNodes(ctx)
  541. req.ParseForm()
  542. renderer, filter, err := r.RendererForTopology(topologyID, req.Form, rpt)
  543. if err != nil {
  544. respondWith(ctx, w, http.StatusInternalServerError, err)
  545. return
  546. }
  547. f(ctx, renderer, filter, RenderContextForReporter(rep, rpt), w, req)
  548. }
  549. }