mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 01:30:30 +00:00
- Add interfaces to allow for alternative implementations for Collector, ControlRouter and PipeRouter. - Pass contexts on http handlers to these interfaces. Although not used by the current (local, in-memory) implementations, the idea is this will be used to pass headers to implementations which support multitenancy (by, for instance, putting an authenticating reverse proxy in form of the app, and then inspecting the headers of the request for a used id).
280 lines
7.4 KiB
Go
280 lines
7.4 KiB
Go
package app
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/gorilla/mux"
|
|
"golang.org/x/net/context"
|
|
|
|
"github.com/weaveworks/scope/render"
|
|
"github.com/weaveworks/scope/report"
|
|
)
|
|
|
|
const apiTopologyURL = "/api/topology/"
|
|
|
|
var (
|
|
topologyRegistry = ®istry{
|
|
items: map[string]APITopologyDesc{},
|
|
}
|
|
kubernetesTopologies = []APITopologyDesc{
|
|
{
|
|
id: "pods",
|
|
renderer: render.PodRenderer,
|
|
Name: "Pods",
|
|
Rank: 3,
|
|
Options: map[string][]APITopologyOption{"system": {
|
|
{"show", "System pods shown", false, render.FilterNoop},
|
|
{"hide", "System pods hidden", true, render.FilterSystem},
|
|
}},
|
|
},
|
|
{
|
|
id: "pods-by-service",
|
|
parent: "pods",
|
|
renderer: render.PodServiceRenderer,
|
|
Name: "by service",
|
|
Options: map[string][]APITopologyOption{"system": {
|
|
{"show", "System services shown", false, render.FilterNoop},
|
|
{"hide", "System services hidden", true, render.FilterSystem},
|
|
}},
|
|
},
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
containerFilters := map[string][]APITopologyOption{
|
|
"system": {
|
|
{"show", "System containers shown", false, render.FilterNoop},
|
|
{"hide", "System containers hidden", true, render.FilterSystem},
|
|
},
|
|
"stopped": {
|
|
{"show", "Stopped containers shown", false, render.FilterNoop},
|
|
{"hide", "Stopped containers hidden", true, render.FilterStopped},
|
|
},
|
|
}
|
|
|
|
// Topology option labels should tell the current state. The first item must
|
|
// be the verb to get to that state
|
|
topologyRegistry.add(
|
|
APITopologyDesc{
|
|
id: "processes",
|
|
renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer),
|
|
Name: "Processes",
|
|
Rank: 1,
|
|
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, render.FilterNoop},
|
|
}},
|
|
},
|
|
APITopologyDesc{
|
|
id: "processes-by-name",
|
|
parent: "processes",
|
|
renderer: render.FilterUnconnected(render.ProcessNameRenderer),
|
|
Name: "by name",
|
|
Options: map[string][]APITopologyOption{"unconnected": {
|
|
// Ditto above.
|
|
{"hide", "Unconnected nodes hidden", true, render.FilterNoop},
|
|
}},
|
|
},
|
|
APITopologyDesc{
|
|
id: "containers",
|
|
renderer: render.ContainerWithImageNameRenderer,
|
|
Name: "Containers",
|
|
Rank: 2,
|
|
Options: containerFilters,
|
|
},
|
|
APITopologyDesc{
|
|
id: "containers-by-image",
|
|
parent: "containers",
|
|
renderer: render.ContainerImageRenderer,
|
|
Name: "by image",
|
|
Options: containerFilters,
|
|
},
|
|
APITopologyDesc{
|
|
id: "containers-by-hostname",
|
|
parent: "containers",
|
|
renderer: render.ContainerHostnameRenderer,
|
|
Name: "by DNS name",
|
|
Options: containerFilters,
|
|
},
|
|
APITopologyDesc{
|
|
id: "hosts",
|
|
renderer: render.HostRenderer,
|
|
Name: "Hosts",
|
|
Rank: 4,
|
|
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 {
|
|
id string
|
|
parent string
|
|
renderer render.Renderer
|
|
|
|
Name string `json:"name"`
|
|
Rank int `json:"rank"`
|
|
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 {
|
|
NodeCount int `json:"node_count"`
|
|
NonpseudoNodeCount int `json:"nonpseudo_node_count"`
|
|
EdgeCount int `json:"edge_count"`
|
|
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 (r *registry) makeTopologyList(rep Reporter) CtxHandlerFunc {
|
|
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
topologies := r.renderTopologies(rep.Report(ctx), req)
|
|
respondWith(w, http.StatusOK, topologies)
|
|
}
|
|
}
|
|
|
|
func (r *registry) renderTopologies(rpt report.Report, req *http.Request) []APITopologyDesc {
|
|
topologies := []APITopologyDesc{}
|
|
r.walk(func(desc APITopologyDesc) {
|
|
renderer := renderedForRequest(req, desc)
|
|
desc.Stats = decorateWithStats(rpt, renderer)
|
|
for i := range desc.SubTopologies {
|
|
renderer := renderedForRequest(req, desc.SubTopologies[i])
|
|
desc.SubTopologies[i].Stats = decorateWithStats(rpt, renderer)
|
|
}
|
|
topologies = append(topologies, desc)
|
|
})
|
|
return topologies
|
|
}
|
|
|
|
func decorateWithStats(rpt report.Report, renderer render.Renderer) topologyStats {
|
|
var (
|
|
nodes int
|
|
realNodes int
|
|
edges int
|
|
)
|
|
for _, n := range renderer.Render(rpt) {
|
|
nodes++
|
|
if !n.Pseudo {
|
|
realNodes++
|
|
}
|
|
edges += len(n.Adjacency)
|
|
}
|
|
renderStats := renderer.Stats(rpt)
|
|
return topologyStats{
|
|
NodeCount: nodes,
|
|
NonpseudoNodeCount: realNodes,
|
|
EdgeCount: edges,
|
|
FilteredNodes: renderStats.FilteredNodes,
|
|
}
|
|
}
|
|
|
|
func (r *registry) enableKubernetesTopologies() {
|
|
r.add(kubernetesTopologies...)
|
|
}
|
|
|
|
func renderedForRequest(r *http.Request, topology APITopologyDesc) render.Renderer {
|
|
renderer := topology.renderer
|
|
for param, opts := range topology.Options {
|
|
value := r.FormValue(param)
|
|
for _, opt := range opts {
|
|
if (value == "" && opt.Default) || (opt.Value != "" && opt.Value == value) {
|
|
renderer = opt.decorator(renderer)
|
|
}
|
|
}
|
|
}
|
|
return renderer
|
|
}
|
|
|
|
type reportRenderHandler func(context.Context, Reporter, render.Renderer, http.ResponseWriter, *http.Request)
|
|
|
|
func (r *registry) captureRenderer(rep Reporter, f reportRenderHandler) CtxHandlerFunc {
|
|
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
topology, ok := r.get(mux.Vars(req)["topology"])
|
|
if !ok {
|
|
http.NotFound(w, req)
|
|
return
|
|
}
|
|
renderer := renderedForRequest(req, topology)
|
|
f(ctx, rep, renderer, w, req)
|
|
}
|
|
}
|
|
|
|
func (r *registry) captureRendererWithoutFilters(rep Reporter, topologyID string, f reportRenderHandler) CtxHandlerFunc {
|
|
return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
|
topology, ok := r.get(topologyID)
|
|
if !ok {
|
|
http.NotFound(w, req)
|
|
return
|
|
}
|
|
f(ctx, rep, topology.renderer, w, req)
|
|
}
|
|
}
|