package agent import ( "fmt" "net/http" "sort" "strings" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" ) // metaExternalSource is the key name for the service instance meta that // defines the external syncing source. This is used by the UI APIs below // to extract this. const metaExternalSource = "external-source" // ServiceSummary is used to summarize a service type ServiceSummary struct { Kind structs.ServiceKind `json:",omitempty"` Name string Tags []string Nodes []string ChecksPassing int ChecksWarning int ChecksCritical int ExternalSources []string externalSourceSet map[string]struct{} // internal to track uniqueness } // UINodes is used to list the nodes in a given datacenter. We return a // NodeDump which provides overview information for all the nodes func (s *HTTPServer) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) { // Parse arguments args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil } // Make the RPC request var out structs.IndexedNodeDump defer setMeta(resp, &out.QueryMeta) RPC: if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil { // Retry the request allowing stale data if no leader if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale { args.AllowStale = true goto RPC } return nil, err } // Use empty list instead of nil for _, info := range out.Dump { if info.Services == nil { info.Services = make([]*structs.NodeService, 0) } if info.Checks == nil { info.Checks = make([]*structs.HealthCheck, 0) } } if out.Dump == nil { out.Dump = make(structs.NodeDump, 0) } return out.Dump, nil } // UINodeInfo is used to get info on a single node in a given datacenter. We return a // NodeInfo which provides overview information for the node func (s *HTTPServer) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) { // Parse arguments args := structs.NodeSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil } // Verify we have some DC, or use the default args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/") if args.Node == "" { resp.WriteHeader(http.StatusBadRequest) fmt.Fprint(resp, "Missing node name") return nil, nil } // Make the RPC request var out structs.IndexedNodeDump defer setMeta(resp, &out.QueryMeta) RPC: if err := s.agent.RPC("Internal.NodeInfo", &args, &out); err != nil { // Retry the request allowing stale data if no leader if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale { args.AllowStale = true goto RPC } return nil, err } // Return only the first entry if len(out.Dump) > 0 { info := out.Dump[0] if info.Services == nil { info.Services = make([]*structs.NodeService, 0) } if info.Checks == nil { info.Checks = make([]*structs.HealthCheck, 0) } return info, nil } resp.WriteHeader(http.StatusNotFound) return nil, nil } // UIServices is used to list the services in a given datacenter. We return a // ServiceSummary which provides overview information for the service func (s *HTTPServer) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) { // Parse arguments args := structs.DCSpecificRequest{} if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { return nil, nil } // Make the RPC request var out structs.IndexedNodeDump defer setMeta(resp, &out.QueryMeta) RPC: if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil { // Retry the request allowing stale data if no leader if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale { args.AllowStale = true goto RPC } return nil, err } // Generate the summary return summarizeServices(out.Dump), nil } func summarizeServices(dump structs.NodeDump) []*ServiceSummary { // Collect the summary information var services []string summary := make(map[string]*ServiceSummary) getService := func(service string) *ServiceSummary { serv, ok := summary[service] if !ok { serv = &ServiceSummary{Name: service} summary[service] = serv services = append(services, service) } return serv } // Aggregate all the node information for _, node := range dump { nodeServices := make([]*ServiceSummary, len(node.Services)) for idx, service := range node.Services { sum := getService(service.Service) sum.Tags = service.Tags sum.Nodes = append(sum.Nodes, node.Node) sum.Kind = service.Kind // If there is an external source, add it to the list of external // sources. We only want to add unique sources so there is extra // accounting here with an unexported field to maintain the set // of sources. if len(service.Meta) > 0 && service.Meta[metaExternalSource] != "" { source := service.Meta[metaExternalSource] if sum.externalSourceSet == nil { sum.externalSourceSet = make(map[string]struct{}) } if _, ok := sum.externalSourceSet[source]; !ok { sum.externalSourceSet[source] = struct{}{} sum.ExternalSources = append(sum.ExternalSources, source) } } nodeServices[idx] = sum } for _, check := range node.Checks { var services []*ServiceSummary if check.ServiceName == "" { services = nodeServices } else { services = []*ServiceSummary{getService(check.ServiceName)} } for _, sum := range services { switch check.Status { case api.HealthPassing: sum.ChecksPassing++ case api.HealthWarning: sum.ChecksWarning++ case api.HealthCritical: sum.ChecksCritical++ } } } } // Return the services in sorted order sort.Strings(services) output := make([]*ServiceSummary, len(summary)) for idx, service := range services { // Sort the nodes sum := summary[service] sort.Strings(sum.Nodes) output[idx] = sum } return output }