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 }