github.com/outbrain/consul@v1.4.5/agent/ui_endpoint.go (about)

     1  package agent
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/consul/agent/structs"
    10  	"github.com/hashicorp/consul/api"
    11  )
    12  
    13  // metaExternalSource is the key name for the service instance meta that
    14  // defines the external syncing source. This is used by the UI APIs below
    15  // to extract this.
    16  const metaExternalSource = "external-source"
    17  
    18  // ServiceSummary is used to summarize a service
    19  type ServiceSummary struct {
    20  	Kind              structs.ServiceKind `json:",omitempty"`
    21  	Name              string
    22  	Tags              []string
    23  	Nodes             []string
    24  	ChecksPassing     int
    25  	ChecksWarning     int
    26  	ChecksCritical    int
    27  	ExternalSources   []string
    28  	externalSourceSet map[string]struct{} // internal to track uniqueness
    29  }
    30  
    31  // UINodes is used to list the nodes in a given datacenter. We return a
    32  // NodeDump which provides overview information for all the nodes
    33  func (s *HTTPServer) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    34  	// Parse arguments
    35  	args := structs.DCSpecificRequest{}
    36  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
    37  		return nil, nil
    38  	}
    39  
    40  	// Make the RPC request
    41  	var out structs.IndexedNodeDump
    42  	defer setMeta(resp, &out.QueryMeta)
    43  RPC:
    44  	if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil {
    45  		// Retry the request allowing stale data if no leader
    46  		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
    47  			args.AllowStale = true
    48  			goto RPC
    49  		}
    50  		return nil, err
    51  	}
    52  
    53  	// Use empty list instead of nil
    54  	for _, info := range out.Dump {
    55  		if info.Services == nil {
    56  			info.Services = make([]*structs.NodeService, 0)
    57  		}
    58  		if info.Checks == nil {
    59  			info.Checks = make([]*structs.HealthCheck, 0)
    60  		}
    61  	}
    62  	if out.Dump == nil {
    63  		out.Dump = make(structs.NodeDump, 0)
    64  	}
    65  	return out.Dump, nil
    66  }
    67  
    68  // UINodeInfo is used to get info on a single node in a given datacenter. We return a
    69  // NodeInfo which provides overview information for the node
    70  func (s *HTTPServer) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
    71  	// Parse arguments
    72  	args := structs.NodeSpecificRequest{}
    73  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
    74  		return nil, nil
    75  	}
    76  
    77  	// Verify we have some DC, or use the default
    78  	args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
    79  	if args.Node == "" {
    80  		resp.WriteHeader(http.StatusBadRequest)
    81  		fmt.Fprint(resp, "Missing node name")
    82  		return nil, nil
    83  	}
    84  
    85  	// Make the RPC request
    86  	var out structs.IndexedNodeDump
    87  	defer setMeta(resp, &out.QueryMeta)
    88  RPC:
    89  	if err := s.agent.RPC("Internal.NodeInfo", &args, &out); err != nil {
    90  		// Retry the request allowing stale data if no leader
    91  		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
    92  			args.AllowStale = true
    93  			goto RPC
    94  		}
    95  		return nil, err
    96  	}
    97  
    98  	// Return only the first entry
    99  	if len(out.Dump) > 0 {
   100  		info := out.Dump[0]
   101  		if info.Services == nil {
   102  			info.Services = make([]*structs.NodeService, 0)
   103  		}
   104  		if info.Checks == nil {
   105  			info.Checks = make([]*structs.HealthCheck, 0)
   106  		}
   107  		return info, nil
   108  	}
   109  
   110  	resp.WriteHeader(http.StatusNotFound)
   111  	return nil, nil
   112  }
   113  
   114  // UIServices is used to list the services in a given datacenter. We return a
   115  // ServiceSummary which provides overview information for the service
   116  func (s *HTTPServer) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
   117  	// Parse arguments
   118  	args := structs.DCSpecificRequest{}
   119  	if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
   120  		return nil, nil
   121  	}
   122  
   123  	// Make the RPC request
   124  	var out structs.IndexedNodeDump
   125  	defer setMeta(resp, &out.QueryMeta)
   126  RPC:
   127  	if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil {
   128  		// Retry the request allowing stale data if no leader
   129  		if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
   130  			args.AllowStale = true
   131  			goto RPC
   132  		}
   133  		return nil, err
   134  	}
   135  
   136  	// Generate the summary
   137  	return summarizeServices(out.Dump), nil
   138  }
   139  
   140  func summarizeServices(dump structs.NodeDump) []*ServiceSummary {
   141  	// Collect the summary information
   142  	var services []string
   143  	summary := make(map[string]*ServiceSummary)
   144  	getService := func(service string) *ServiceSummary {
   145  		serv, ok := summary[service]
   146  		if !ok {
   147  			serv = &ServiceSummary{Name: service}
   148  			summary[service] = serv
   149  			services = append(services, service)
   150  		}
   151  		return serv
   152  	}
   153  
   154  	// Aggregate all the node information
   155  	for _, node := range dump {
   156  		nodeServices := make([]*ServiceSummary, len(node.Services))
   157  		for idx, service := range node.Services {
   158  			sum := getService(service.Service)
   159  			sum.Tags = service.Tags
   160  			sum.Nodes = append(sum.Nodes, node.Node)
   161  			sum.Kind = service.Kind
   162  
   163  			// If there is an external source, add it to the list of external
   164  			// sources. We only want to add unique sources so there is extra
   165  			// accounting here with an unexported field to maintain the set
   166  			// of sources.
   167  			if len(service.Meta) > 0 && service.Meta[metaExternalSource] != "" {
   168  				source := service.Meta[metaExternalSource]
   169  				if sum.externalSourceSet == nil {
   170  					sum.externalSourceSet = make(map[string]struct{})
   171  				}
   172  				if _, ok := sum.externalSourceSet[source]; !ok {
   173  					sum.externalSourceSet[source] = struct{}{}
   174  					sum.ExternalSources = append(sum.ExternalSources, source)
   175  				}
   176  			}
   177  
   178  			nodeServices[idx] = sum
   179  		}
   180  		for _, check := range node.Checks {
   181  			var services []*ServiceSummary
   182  			if check.ServiceName == "" {
   183  				services = nodeServices
   184  			} else {
   185  				services = []*ServiceSummary{getService(check.ServiceName)}
   186  			}
   187  			for _, sum := range services {
   188  				switch check.Status {
   189  				case api.HealthPassing:
   190  					sum.ChecksPassing++
   191  				case api.HealthWarning:
   192  					sum.ChecksWarning++
   193  				case api.HealthCritical:
   194  					sum.ChecksCritical++
   195  				}
   196  			}
   197  		}
   198  	}
   199  
   200  	// Return the services in sorted order
   201  	sort.Strings(services)
   202  	output := make([]*ServiceSummary, len(summary))
   203  	for idx, service := range services {
   204  		// Sort the nodes
   205  		sum := summary[service]
   206  		sort.Strings(sum.Nodes)
   207  		output[idx] = sum
   208  	}
   209  	return output
   210  }