diff --git a/app/api_topologies.go b/app/api_topologies.go index e5495a772..6052f86af 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -2,26 +2,140 @@ package main import ( "net/http" + "sort" + "sync" + + "github.com/gorilla/mux" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/xfer" ) +const apiTopologyURL = "/api/topology/" + +var ( + topologyRegistry = ®istry{ + items: map[string]APITopologyDesc{}, + } + kubernetesTopologies = []APITopologyDesc{ + { + id: "pods", + renderer: render.PodRenderer, + Name: "Pods", + Options: map[string][]APITopologyOption{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, + { + id: "pods-by-service", + parent: "pods", + renderer: render.PodServiceRenderer, + Name: "by service", + Options: map[string][]APITopologyOption{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, + } +) + +func init() { + // Topology option labels should tell the current state. The first item must + // be the verb to get to that state + topologyRegistry.add( + APITopologyDesc{ + id: "applications", + renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), + Name: "Applications", + 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}, + }}, + }, + APITopologyDesc{ + id: "applications-by-name", + parent: "applications", + renderer: render.FilterUnconnected(render.ProcessNameRenderer), + Name: "by name", + Options: map[string][]APITopologyOption{"unconnected": { + // Ditto above. + {"hide", "Unconnected nodes hidden", true, nop}, + }}, + }, + 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}, + }}, + }, + 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}, + }}, + }, + 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}, + }}, + }, + APITopologyDesc{ + id: "hosts", + renderer: render.HostRenderer, + Name: "Hosts", + Options: map[string][]APITopologyOption{}, + }, + ) +} + +// registry is a threadsafe store of the available topologies +type registry struct { + sync.RWMutex + items map[string]APITopologyDesc +} + // APITopologyDesc is returned in a list by the /api/topology handler. type APITopologyDesc struct { - Name string `json:"name"` - URL string `json:"url"` - SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"` - Options map[string][]APITopologyOption `json:"options"` - Stats *topologyStats `json:"stats,omitempty"` + id string + parent string + renderer render.Renderer + + Name string `json:"name"` + Options map[string][]APITopologyOption `json:"options"` + + URL string `json:"url"` + SubTopologies []APITopologyDesc `json:"sub_topologies,omitempty"` + Stats *topologyStats `json:"stats,omitempty"` } +type byName []APITopologyDesc + +func (a byName) Len() int { return len(a) } +func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byName) Less(i, j int) bool { return a[i].Name < a[j].Name } + // APITopologyOption describes a ¶m=value to a given topology. type APITopologyOption struct { Value string `json:"value"` Display string `json:"display"` Default bool `json:"default,omitempty"` + + decorator func(render.Renderer) render.Renderer } type topologyStats struct { @@ -31,71 +145,113 @@ type topologyStats struct { FilteredNodes int `json:"filtered_nodes"` } +func (r *registry) add(ts ...APITopologyDesc) { + r.Lock() + defer r.Unlock() + for _, t := range ts { + t.URL = apiTopologyURL + t.id + + if t.parent != "" { + parent := r.items[t.parent] + parent.SubTopologies = append(parent.SubTopologies, t) + sort.Sort(byName(parent.SubTopologies)) + r.items[t.parent] = parent + } + + r.items[t.id] = t + } +} + +func (r *registry) get(name string) (APITopologyDesc, bool) { + r.RLock() + defer r.RUnlock() + t, ok := r.items[name] + return t, ok +} + +func (r *registry) walk(f func(APITopologyDesc)) { + r.RLock() + defer r.RUnlock() + descs := []APITopologyDesc{} + for _, desc := range r.items { + if desc.parent != "" { + continue + } + descs = append(descs, desc) + } + sort.Sort(byName(descs)) + for _, desc := range descs { + f(desc) + } +} + // makeTopologyList returns a handler that yields an APITopologyList. -func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { +func (r *registry) makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { var ( rpt = rep.Report() topologies = []APITopologyDesc{} ) - topologyRegistry.walk(func(name string, def topologyView, subDefs map[string]topologyView) { - describedSubDefs := []APITopologyDesc{} - for subName, subDef := range subDefs { - describedSubDefs = append(describedSubDefs, APITopologyDesc{ - Name: subDef.human, - URL: "/api/topology/" + subName, - Options: makeTopologyOptions(subDef), - Stats: stats(subDef.renderer, rpt), - }) + r.walk(func(desc APITopologyDesc) { + decorateTopologyForRequest(req, &desc) + decorateWithStats(&desc, rpt) + for i := range desc.SubTopologies { + decorateTopologyForRequest(req, &desc.SubTopologies[i]) + decorateWithStats(&desc.SubTopologies[i], rpt) } - topologies = append(topologies, APITopologyDesc{ - Name: def.human, - URL: "/api/topology/" + name, - SubTopologies: describedSubDefs, - Options: makeTopologyOptions(def), - Stats: stats(def.renderer, rpt), - }) + topologies = append(topologies, desc) }) - respondWith(w, http.StatusOK, topologies) } } -func makeTopologyOptions(view topologyView) map[string][]APITopologyOption { - options := map[string][]APITopologyOption{} - for param, optionVals := range view.options { - for _, optionVal := range optionVals { - options[param] = append(options[param], APITopologyOption{ - Value: optionVal.value, - Display: optionVal.human, - Default: optionVal.def, - }) - } - } - return options -} - -func stats(renderer render.Renderer, rpt report.Report) *topologyStats { +func decorateWithStats(desc *APITopologyDesc, rpt report.Report) { var ( nodes int realNodes int edges int ) - - for _, n := range renderer.Render(rpt) { + for _, n := range desc.renderer.Render(rpt) { nodes++ if !n.Pseudo { realNodes++ } edges += len(n.Adjacency) } - - renderStats := renderer.Stats(rpt) - - return &topologyStats{ + renderStats := desc.renderer.Stats(rpt) + desc.Stats = &topologyStats{ NodeCount: nodes, NonpseudoNodeCount: realNodes, EdgeCount: edges, FilteredNodes: renderStats.FilteredNodes, } } + +func nop(r render.Renderer) render.Renderer { return r } + +func (r *registry) enableKubernetesTopologies() { + r.add(kubernetesTopologies...) +} + +func decorateTopologyForRequest(r *http.Request, topology *APITopologyDesc) { + for param, opts := range topology.Options { + value := r.FormValue(param) + for _, opt := range opts { + if (value == "" && opt.Default) || (opt.Value != "" && opt.Value == value) { + topology.renderer = opt.decorator(topology.renderer) + } + } + } +} + +func (r *registry) captureTopology(rep xfer.Reporter, f func(xfer.Reporter, APITopologyDesc, http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + topology, ok := r.get(mux.Vars(req)["topology"]) + if !ok { + http.NotFound(w, req) + return + } + decorateTopologyForRequest(req, &topology) + f(rep, topology, w, req) + } +} diff --git a/app/api_topology.go b/app/api_topology.go index 03f96934e..5e9dad197 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -33,14 +33,14 @@ type APIEdge struct { } // Full topology. -func handleTopology(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) { +func handleTopology(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) { respondWith(w, http.StatusOK, APITopology{ Nodes: t.renderer.Render(rep.Report()).Prune(), }) } // Websocket for the full topology. This route overlaps with the next. -func handleWs(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) { +func handleWs(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { respondWith(w, http.StatusInternalServerError, err.Error()) return @@ -57,7 +57,7 @@ func handleWs(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http. } // Individual nodes. -func handleNode(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) { +func handleNode(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) { var ( vars = mux.Vars(r) nodeID = vars["id"] @@ -72,7 +72,7 @@ func handleNode(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *htt } // Individual edges. -func handleEdge(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *http.Request) { +func handleEdge(rep xfer.Reporter, t APITopologyDesc, w http.ResponseWriter, r *http.Request) { var ( vars = mux.Vars(r) localID = vars["local"] @@ -92,7 +92,7 @@ func handleWebsocket( w http.ResponseWriter, r *http.Request, rep xfer.Reporter, - t topologyView, + t APITopologyDesc, loop time.Duration, ) { conn, err := upgrader.Upgrade(w, r, nil) diff --git a/app/router.go b/app/router.go index 93f58a1cd..340c4b837 100644 --- a/app/router.go +++ b/app/router.go @@ -6,12 +6,10 @@ import ( "net/http" "net/url" "strings" - "sync" "github.com/PuerkitoBio/ghost/handlers" "github.com/gorilla/mux" - "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/xfer" ) @@ -65,12 +63,17 @@ func Router(c collector) *mux.Router { get := router.Methods("GET").Subrouter() get.HandleFunc("/api", gzipHandler(apiHandler)) - get.HandleFunc("/api/topology", gzipHandler(makeTopologyList(c))) - get.HandleFunc("/api/topology/{topology}", gzipHandler(captureTopology(c, handleTopology))) - get.HandleFunc("/api/topology/{topology}/ws", captureTopology(c, handleWs)) // NB not gzip! - get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(gzipHandler(captureTopology(c, handleNode))) - get.MatcherFunc(URLMatcher("/api/topology/{topology}/{local}/{remote}")).HandlerFunc(gzipHandler(captureTopology(c, handleEdge))) - get.MatcherFunc(URLMatcher("/api/origin/host/{id}")).HandlerFunc(gzipHandler(makeOriginHostHandler(c))) + get.HandleFunc("/api/topology", gzipHandler(topologyRegistry.makeTopologyList(c))) + get.HandleFunc("/api/topology/{topology}", + gzipHandler(topologyRegistry.captureTopology(c, handleTopology))) + get.HandleFunc("/api/topology/{topology}/ws", + topologyRegistry.captureTopology(c, handleWs)) // NB not gzip! + get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc( + gzipHandler(topologyRegistry.captureTopology(c, handleNode))) + get.MatcherFunc(URLMatcher("/api/topology/{topology}/{local}/{remote}")).HandlerFunc( + gzipHandler(topologyRegistry.captureTopology(c, handleEdge))) + get.MatcherFunc(URLMatcher("/api/origin/host/{id}")).HandlerFunc( + gzipHandler(makeOriginHostHandler(c))) get.HandleFunc("/api/report", gzipHandler(makeRawReportHandler(c))) get.PathPrefix("/").Handler(http.FileServer(FS(false))) // everything else is static @@ -98,35 +101,12 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc { } a.Add(rpt) if len(rpt.Pod.Nodes) > 0 { - enableKubernetesTopologies() + topologyRegistry.enableKubernetesTopologies() } w.WriteHeader(http.StatusOK) } } -func decorateTopologyForRequest(r *http.Request, topology *topologyView) { - for param, opts := range topology.options { - value := r.FormValue(param) - for _, opt := range opts { - if (value == "" && opt.def) || (opt.value != "" && opt.value == value) { - topology.renderer = opt.decorator(topology.renderer) - } - } - } -} - -func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http.ResponseWriter, *http.Request)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - topology, ok := topologyRegistry.get(mux.Vars(r)["topology"]) - if !ok { - http.NotFound(w, r) - return - } - decorateTopologyForRequest(r, &topology) - f(rep, topology, w, r) - } -} - // APIDetails are some generic details that can be fetched from /api type APIDetails struct { ID string `json:"id"` @@ -136,150 +116,3 @@ type APIDetails struct { func apiHandler(w http.ResponseWriter, r *http.Request) { respondWith(w, http.StatusOK, APIDetails{ID: uniqueID, Version: version}) } - -// registry is a threadsafe store of the available topologies -type registry struct { - sync.RWMutex - items map[string]topologyView -} - -func (r *registry) add(ts map[string]topologyView) { - r.Lock() - defer r.Unlock() - result := map[string]topologyView{} - for name, t := range r.items { - result[name] = t - } - for name, t := range ts { - result[name] = t - } - r.items = result -} - -func (r *registry) get(name string) (topologyView, bool) { - r.RLock() - defer r.RUnlock() - t, ok := r.items[name] - return t, ok -} - -func (r *registry) walk(f func(string, topologyView, map[string]topologyView)) { - r.RLock() - defer r.RUnlock() - for name, def := range r.items { - if def.parent != "" { - continue - } - - subDefs := map[string]topologyView{} - for subName, subDef := range r.items { - if subDef.parent != name { - continue - } - subDefs[subName] = subDef - } - - f(name, def, subDefs) - } -} - -// Topology option labels should tell the current state. The first item must -// be the verb to get to that state -var topologyRegistry = ®istry{ - items: map[string]topologyView{ - "applications": { - human: "Applications", - parent: "", - renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), - options: optionParams{"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}, - }}, - }, - "applications-by-name": { - human: "by name", - parent: "applications", - renderer: render.FilterUnconnected(render.ProcessNameRenderer), - options: optionParams{"unconnected": { - // Ditto above. - {"hide", "Unconnected nodes hidden", true, nop}, - }}, - }, - "containers": { - human: "Containers", - parent: "", - renderer: render.ContainerWithImageNameRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "containers-by-image": { - human: "by image", - parent: "containers", - renderer: render.ContainerImageRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "containers-by-hostname": { - human: "by hostname", - parent: "containers", - renderer: render.ContainerHostnameRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "hosts": { - human: "Hosts", - parent: "", - renderer: render.HostRenderer, - }, - }, -} - -type topologyView struct { - human string - parent string - renderer render.Renderer - options optionParams -} - -type optionParams map[string][]optionValue // param: values - -type optionValue struct { - value string // "hide" - human string // "Hide system containers" - def bool - decorator func(render.Renderer) render.Renderer -} - -func nop(r render.Renderer) render.Renderer { return r } - -func enableKubernetesTopologies() { - topologyRegistry.add(kubernetesTopologies) -} - -var kubernetesTopologies = map[string]topologyView{ - "pods": { - human: "Pods", - parent: "", - renderer: render.PodRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "pods-by-service": { - human: "by service", - parent: "pods", - renderer: render.PodServiceRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, -}