123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- package app
- import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "sort"
- "strings"
- "sync"
- "time"
- "github.com/gorilla/mux"
- opentracing "github.com/opentracing/opentracing-go"
- log "github.com/sirupsen/logrus"
- "github.com/weaveworks/scope/probe/docker"
- "github.com/weaveworks/scope/probe/kubernetes"
- "github.com/weaveworks/scope/render"
- "github.com/weaveworks/scope/report"
- )
- const (
- apiTopologyURL = "/api/topology/"
- processesID = "processes"
- processesByNameID = "processes-by-name"
- systemGroupID = "system"
- containersID = "containers"
- containersByHostnameID = "containers-by-hostname"
- containersByImageID = "containers-by-image"
- podsID = "pods"
- kubeControllersID = "kube-controllers"
- servicesID = "services"
- hostsID = "hosts"
- weaveID = "weave"
- ecsTasksID = "ecs-tasks"
- ecsServicesID = "ecs-services"
- swarmServicesID = "swarm-services"
- )
- var (
- topologyRegistry = MakeRegistry()
- unmanagedFilter = APITopologyOptionGroup{
- ID: "pseudo",
- Default: "hide",
- Options: []APITopologyOption{
- {Value: "show", Label: "Show unmanaged", filter: nil, filterPseudo: false},
- {Value: "hide", Label: "Hide unmanaged", filter: render.IsNotPseudo, filterPseudo: true},
- },
- }
- storageFilter = APITopologyOptionGroup{
- ID: "storage",
- Default: "hide",
- Options: []APITopologyOption{
- {Value: "show", Label: "Show storage", filter: nil, filterPseudo: false},
- {Value: "hide", Label: "Hide storage", filter: render.IsPodComponent, filterPseudo: false},
- },
- }
- snapshotFilter = APITopologyOptionGroup{
- ID: "snapshot",
- Default: "hide",
- Options: []APITopologyOption{
- {Value: "show", Label: "Show snapshots", filter: nil, filterPseudo: false},
- {Value: "hide", Label: "Hide snapshots", filter: render.IsNonSnapshotComponent, filterPseudo: false},
- },
- }
- )
- // namespaceFilters generates a namespace selector option group based on the given namespaces
- func namespaceFilters(namespaces []string, noneLabel string) APITopologyOptionGroup {
- options := APITopologyOptionGroup{ID: "namespace", Default: "", SelectType: "union", NoneLabel: noneLabel}
- for _, namespace := range namespaces {
- options.Options = append(options.Options, APITopologyOption{
- Value: namespace, Label: namespace, filter: render.IsNamespace(namespace), filterPseudo: false,
- })
- }
- return options
- }
- // updateFilters updates the available filters based on the current report.
- func updateFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
- topologies = updateKubeFilters(rpt, topologies)
- topologies = updateSwarmFilters(rpt, topologies)
- return topologies
- }
- func updateSwarmFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
- namespaces := map[string]struct{}{}
- for _, n := range rpt.SwarmService.Nodes {
- if namespace, ok := n.Latest.Lookup(docker.StackNamespace); ok {
- namespaces[namespace] = struct{}{}
- }
- }
- if len(namespaces) == 0 {
- // We only want to apply filters when we have swarm-related nodes,
- // so if we don't then return early
- return topologies
- }
- ns := []string{}
- for namespace := range namespaces {
- ns = append(ns, namespace)
- }
- topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely
- for i, t := range topologies {
- if t.id == containersID || t.id == swarmServicesID {
- topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{
- namespaceFilters(ns, "All Stacks"),
- })
- }
- }
- return topologies
- }
- func updateKubeFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc {
- ns := []string{}
- for _, n := range rpt.Namespace.Nodes {
- name, ok := n.Latest.Lookup(kubernetes.Name)
- if !ok {
- continue
- }
- ns = append(ns, name)
- }
- if len(ns) == 0 {
- return topologies
- }
- sort.Strings(ns)
- topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely
- for i, t := range topologies {
- if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == kubeControllersID {
- topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{
- namespaceFilters(ns, "All Namespaces"),
- })
- }
- }
- return topologies
- }
- // mergeTopologyFilters recursively merges in new options on a topology description
- func mergeTopologyFilters(t APITopologyDesc, options []APITopologyOptionGroup) APITopologyDesc {
- t.Options = append(append([]APITopologyOptionGroup{}, t.Options...), options...)
- newSubTopologies := make([]APITopologyDesc, len(t.SubTopologies))
- for i, sub := range t.SubTopologies {
- newSubTopologies[i] = mergeTopologyFilters(sub, options)
- }
- t.SubTopologies = newSubTopologies
- return t
- }
- // MakeAPITopologyOption provides an external interface to the package for creating an APITopologyOption.
- func MakeAPITopologyOption(value string, label string, filterFunc render.FilterFunc, pseudo bool) APITopologyOption {
- return APITopologyOption{Value: value, Label: label, filter: filterFunc, filterPseudo: pseudo}
- }
- // Registry is a threadsafe store of the available topologies
- type Registry struct {
- sync.RWMutex
- items map[string]APITopologyDesc
- }
- // MakeRegistry returns a new Registry
- func MakeRegistry() *Registry {
- registry := &Registry{
- items: map[string]APITopologyDesc{},
- }
- containerFilters := []APITopologyOptionGroup{
- {
- ID: systemGroupID,
- Default: "application",
- Options: []APITopologyOption{
- {Value: "all", Label: "All", filter: nil, filterPseudo: false},
- {Value: "system", Label: "System containers", filter: render.IsSystem, filterPseudo: false},
- {Value: "application", Label: "Application containers", filter: render.IsApplication, filterPseudo: false}},
- },
- {
- ID: "stopped",
- Default: "running",
- Options: []APITopologyOption{
- {Value: "stopped", Label: "Stopped containers", filter: render.IsStopped, filterPseudo: false},
- {Value: "running", Label: "Running containers", filter: render.IsRunning, filterPseudo: false},
- {Value: "both", Label: "Both", filter: nil, filterPseudo: false},
- },
- },
- {
- ID: "pseudo",
- Default: "hide",
- Options: []APITopologyOption{
- {Value: "show", Label: "Show uncontained", filter: nil, filterPseudo: false},
- {Value: "hide", Label: "Hide uncontained", filter: render.IsNotPseudo, filterPseudo: true},
- },
- },
- }
- unconnectedFilter := []APITopologyOptionGroup{
- {
- ID: "unconnected",
- Default: "hide",
- Options: []APITopologyOption{
- {Value: "show", Label: "Show unconnected", filter: nil, filterPseudo: false},
- {Value: "hide", Label: "Hide unconnected", filter: render.IsConnected, filterPseudo: false},
- },
- },
- }
- // Topology option labels should tell the current state. The first item must
- // be the verb to get to that state
- registry.Add(
- APITopologyDesc{
- id: processesID,
- renderer: render.ConnectedProcessRenderer,
- Name: "Processes",
- Rank: 1,
- Options: unconnectedFilter,
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: processesByNameID,
- parent: processesID,
- renderer: render.ProcessNameRenderer,
- Name: "by name",
- Options: unconnectedFilter,
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: containersID,
- renderer: render.ContainerWithImageNameRenderer,
- Name: "Containers",
- Rank: 2,
- Options: containerFilters,
- },
- APITopologyDesc{
- id: containersByHostnameID,
- parent: containersID,
- renderer: render.ContainerHostnameRenderer,
- Name: "by DNS name",
- Options: containerFilters,
- },
- APITopologyDesc{
- id: containersByImageID,
- parent: containersID,
- renderer: render.ContainerImageRenderer,
- Name: "by image",
- Options: containerFilters,
- },
- APITopologyDesc{
- id: podsID,
- renderer: render.PodRenderer,
- Name: "Pods",
- Rank: 3,
- Options: []APITopologyOptionGroup{snapshotFilter, storageFilter, unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: kubeControllersID,
- parent: podsID,
- renderer: render.KubeControllerRenderer,
- Name: "Controllers",
- Options: []APITopologyOptionGroup{unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: servicesID,
- parent: podsID,
- renderer: render.PodServiceRenderer,
- Name: "Services",
- Options: []APITopologyOptionGroup{unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: ecsTasksID,
- renderer: render.ECSTaskRenderer,
- Name: "Tasks",
- Rank: 3,
- Options: []APITopologyOptionGroup{unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: ecsServicesID,
- parent: ecsTasksID,
- renderer: render.ECSServiceRenderer,
- Name: "Services",
- Options: []APITopologyOptionGroup{unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: swarmServicesID,
- renderer: render.SwarmServiceRenderer,
- Name: "Services",
- Rank: 3,
- Options: []APITopologyOptionGroup{unmanagedFilter},
- HideIfEmpty: true,
- },
- APITopologyDesc{
- id: hostsID,
- renderer: render.HostRenderer,
- Name: "Hosts",
- Rank: 4,
- },
- APITopologyDesc{
- id: weaveID,
- parent: hostsID,
- renderer: render.WeaveRenderer,
- Name: "Weave Net",
- },
- )
- return registry
- }
- // 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"`
- HideIfEmpty bool `json:"hide_if_empty"`
- Options []APITopologyOptionGroup `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 }
- // APITopologyOptionGroup describes a group of APITopologyOptions
- type APITopologyOptionGroup struct {
- ID string `json:"id"`
- // Default value for the option. Used if the value is omitted; not used if the value is ""
- Default string `json:"defaultValue"`
- Options []APITopologyOption `json:"options,omitempty"`
- // SelectType describes how options can be picked. Currently defined values:
- // "one": Default if empty. Exactly one option may be picked from the list.
- // "union": Any number of options may be picked. Nodes matching any option filter selected are displayed.
- // Value and Default should be a ","-separated list.
- SelectType string `json:"selectType,omitempty"`
- // For "union" type, this is the label the UI should use to represent the case where nothing is selected
- NoneLabel string `json:"noneLabel,omitempty"`
- }
- // Get the render filters to use for this option group, if any, or nil otherwise.
- func (g APITopologyOptionGroup) filter(value string) render.FilterFunc {
- var values []string
- switch g.SelectType {
- case "", "one":
- values = []string{value}
- case "union":
- values = strings.Split(value, ",")
- default:
- log.Errorf("Invalid select type %s for option group %s, ignoring option", g.SelectType, g.ID)
- return nil
- }
- filters := []render.FilterFunc{}
- for _, opt := range g.Options {
- for _, v := range values {
- if v != opt.Value {
- continue
- }
- var filter render.FilterFunc
- if opt.filter == nil {
- // No filter means match everything (pseudo doesn't matter)
- filter = func(n report.Node) bool { return true }
- } else if opt.filterPseudo {
- // Apply filter to pseudo topologies also
- filter = opt.filter
- } else {
- // Allow all pseudo topology nodes, only apply filter to non-pseudo
- filter = render.AnyFilterFunc(render.IsPseudoTopology, opt.filter)
- }
- filters = append(filters, filter)
- }
- }
- if len(filters) == 0 {
- return nil
- }
- return render.AnyFilterFunc(filters...)
- }
- // APITopologyOption describes a ¶m=value to a given topology.
- type APITopologyOption struct {
- Value string `json:"value"`
- Label string `json:"label"`
- filter render.FilterFunc
- filterPseudo bool
- }
- type topologyStats struct {
- NodeCount int `json:"node_count"`
- NonpseudoNodeCount int `json:"nonpseudo_node_count"`
- EdgeCount int `json:"edge_count"`
- FilteredNodes int `json:"filtered_nodes"`
- }
- // deserializeTimestamp converts the ISO8601 query param into a proper timestamp.
- func deserializeTimestamp(timestamp string) time.Time {
- if timestamp != "" {
- result, err := time.Parse(time.RFC3339, timestamp)
- if err != nil {
- log.Errorf("Error parsing timestamp '%s' - make sure the time format is correct", timestamp)
- }
- return result
- }
- // Default to current time if no timestamp is provided.
- return time.Now()
- }
- // AddContainerFilters adds to the default Registry (topologyRegistry)'s containerFilters
- func AddContainerFilters(newFilters ...APITopologyOption) {
- topologyRegistry.AddContainerFilters(newFilters...)
- }
- // AddContainerFilters adds container filters to this Registry
- func (r *Registry) AddContainerFilters(newFilters ...APITopologyOption) {
- r.Lock()
- defer r.Unlock()
- for _, key := range []string{containersID, containersByHostnameID, containersByImageID} {
- for i := range r.items[key].Options {
- if r.items[key].Options[i].ID == systemGroupID {
- r.items[key].Options[i].Options = append(r.items[key].Options[i].Options, newFilters...)
- break
- }
- }
- }
- }
- // Add inserts a topologyDesc to the Registry's items map
- func (r *Registry) Add(ts ...APITopologyDesc) {
- r.Lock()
- defer r.Unlock()
- for _, t := range ts {
- t.URL = apiTopologyURL + t.id
- t.renderer = render.Memoise(t.renderer)
- if t.parent != "" {
- parent := r.items[t.parent]
- parent.SubTopologies = append(parent.SubTopologies, t)
- 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) {
- timestamp := deserializeTimestamp(req.URL.Query().Get("timestamp"))
- report, err := rep.Report(ctx, timestamp)
- if err != nil {
- respondWith(ctx, w, http.StatusInternalServerError, err)
- return
- }
- report.UnsafeRemovePartMergedNodes(ctx)
- respondWith(ctx, w, http.StatusOK, r.renderTopologies(ctx, report, req))
- }
- }
- func (r *Registry) renderTopologies(ctx context.Context, rpt report.Report, req *http.Request) []APITopologyDesc {
- span, ctx := opentracing.StartSpanFromContext(ctx, "app.renderTopologies")
- defer span.Finish()
- topologies := []APITopologyDesc{}
- req.ParseForm()
- r.walk(func(desc APITopologyDesc) {
- if ctx.Err() != nil {
- return
- }
- renderer, filter, _ := r.RendererForTopology(desc.id, req.Form, rpt)
- desc.Stats = computeStats(ctx, rpt, renderer, filter)
- for i, sub := range desc.SubTopologies {
- renderer, filter, _ := r.RendererForTopology(sub.id, req.Form, rpt)
- desc.SubTopologies[i].Stats = computeStats(ctx, rpt, renderer, filter)
- }
- topologies = append(topologies, desc)
- })
- return updateFilters(rpt, topologies)
- }
- func computeStats(ctx context.Context, rpt report.Report, renderer render.Renderer, transformer render.Transformer) topologyStats {
- span, ctx := opentracing.StartSpanFromContext(ctx, "app.computeStats")
- defer span.Finish()
- var (
- nodes int
- realNodes int
- edges int
- )
- r := render.Render(ctx, rpt, renderer, transformer)
- for _, n := range r.Nodes {
- nodes++
- if n.Topology != render.Pseudo {
- realNodes++
- }
- edges += len(n.Adjacency)
- }
- return topologyStats{
- NodeCount: nodes,
- NonpseudoNodeCount: realNodes,
- EdgeCount: edges,
- FilteredNodes: r.Filtered,
- }
- }
- // RendererForTopology ..
- func (r *Registry) RendererForTopology(topologyID string, values url.Values, rpt report.Report) (render.Renderer, render.Transformer, error) {
- topology, ok := r.get(topologyID)
- if !ok {
- return nil, nil, fmt.Errorf("topology not found: %s", topologyID)
- }
- topology = updateFilters(rpt, []APITopologyDesc{topology})[0]
- if len(values) == 0 {
- // if no options where provided, only apply base filter
- return topology.renderer, render.FilterUnconnectedPseudo, nil
- }
- var filters []render.FilterFunc
- for _, group := range topology.Options {
- value := group.Default
- if vs := values[group.ID]; len(vs) > 0 {
- value = vs[0]
- }
- if filter := group.filter(value); filter != nil {
- filters = append(filters, filter)
- }
- }
- if len(filters) > 0 {
- return topology.renderer, render.Transformers([]render.Transformer{render.ComposeFilterFuncs(filters...), render.FilterUnconnectedPseudo}), nil
- }
- return topology.renderer, render.FilterUnconnectedPseudo, nil
- }
- type reporterHandler func(context.Context, Reporter, http.ResponseWriter, *http.Request)
- func captureReporter(rep Reporter, f reporterHandler) CtxHandlerFunc {
- return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
- f(ctx, rep, w, r)
- }
- }
- func (r *Registry) captureRenderer(rep Reporter, f rendererHandler) CtxHandlerFunc {
- return func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
- var (
- topologyID = mux.Vars(req)["topology"]
- timestamp = deserializeTimestamp(req.URL.Query().Get("timestamp"))
- )
- if _, ok := r.get(topologyID); !ok {
- http.NotFound(w, req)
- return
- }
- rpt, err := rep.Report(ctx, timestamp)
- if err != nil {
- respondWith(ctx, w, http.StatusInternalServerError, err)
- return
- }
- rpt.UnsafeRemovePartMergedNodes(ctx)
- req.ParseForm()
- renderer, filter, err := r.RendererForTopology(topologyID, req.Form, rpt)
- if err != nil {
- respondWith(ctx, w, http.StatusInternalServerError, err)
- return
- }
- f(ctx, renderer, filter, RenderContextForReporter(rep, rpt), w, req)
- }
- }
|