1package dependency 2 3import ( 4 "encoding/gob" 5 "fmt" 6 "log" 7 "net/url" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/hashicorp/consul/api" 13 "github.com/pkg/errors" 14) 15 16const ( 17 HealthAny = "any" 18 HealthPassing = "passing" 19 HealthWarning = "warning" 20 HealthCritical = "critical" 21 HealthMaint = "maintenance" 22 23 NodeMaint = "_node_maintenance" 24 ServiceMaint = "_service_maintenance:" 25) 26 27var ( 28 // Ensure implements 29 _ Dependency = (*HealthServiceQuery)(nil) 30 31 // HealthServiceQueryRe is the regular expression to use. 32 HealthServiceQueryRe = regexp.MustCompile(`\A` + tagRe + serviceNameRe + dcRe + nearRe + filterRe + `\z`) 33) 34 35func init() { 36 gob.Register([]*HealthService{}) 37} 38 39// HealthService is a service entry in Consul. 40type HealthService struct { 41 Node string 42 NodeID string 43 NodeAddress string 44 NodeTaggedAddresses map[string]string 45 NodeMeta map[string]string 46 ServiceMeta map[string]string 47 Address string 48 ID string 49 Name string 50 Tags ServiceTags 51 Checks api.HealthChecks 52 Status string 53 Port int 54} 55 56// HealthServiceQuery is the representation of all a service query in Consul. 57type HealthServiceQuery struct { 58 stopCh chan struct{} 59 60 dc string 61 filters []string 62 name string 63 near string 64 tag string 65} 66 67// NewHealthServiceQuery processes the strings to build a service dependency. 68func NewHealthServiceQuery(s string) (*HealthServiceQuery, error) { 69 if !HealthServiceQueryRe.MatchString(s) { 70 return nil, fmt.Errorf("health.service: invalid format: %q", s) 71 } 72 73 m := regexpMatch(HealthServiceQueryRe, s) 74 75 var filters []string 76 if filter := m["filter"]; filter != "" { 77 split := strings.Split(filter, ",") 78 for _, f := range split { 79 f = strings.TrimSpace(f) 80 switch f { 81 case HealthAny, 82 HealthPassing, 83 HealthWarning, 84 HealthCritical, 85 HealthMaint: 86 filters = append(filters, f) 87 case "": 88 default: 89 return nil, fmt.Errorf("health.service: invalid filter: %q in %q", f, s) 90 } 91 } 92 sort.Strings(filters) 93 } else { 94 filters = []string{HealthPassing} 95 } 96 97 return &HealthServiceQuery{ 98 stopCh: make(chan struct{}, 1), 99 dc: m["dc"], 100 filters: filters, 101 name: m["name"], 102 near: m["near"], 103 tag: m["tag"], 104 }, nil 105} 106 107// Fetch queries the Consul API defined by the given client and returns a slice 108// of HealthService objects. 109func (d *HealthServiceQuery) Fetch(clients *ClientSet, opts *QueryOptions) (interface{}, *ResponseMetadata, error) { 110 select { 111 case <-d.stopCh: 112 return nil, nil, ErrStopped 113 default: 114 } 115 116 opts = opts.Merge(&QueryOptions{ 117 Datacenter: d.dc, 118 Near: d.near, 119 }) 120 121 u := &url.URL{ 122 Path: "/v1/health/service/" + d.name, 123 RawQuery: opts.String(), 124 } 125 if d.tag != "" { 126 q := u.Query() 127 q.Set("tag", d.tag) 128 u.RawQuery = q.Encode() 129 } 130 log.Printf("[TRACE] %s: GET %s", d, u) 131 132 // Check if a user-supplied filter was given. If so, we may be querying for 133 // more than healthy services, so we need to implement client-side filtering. 134 passingOnly := len(d.filters) == 1 && d.filters[0] == HealthPassing 135 136 entries, qm, err := clients.Consul().Health().Service(d.name, d.tag, passingOnly, opts.ToConsulOpts()) 137 if err != nil { 138 return nil, nil, errors.Wrap(err, d.String()) 139 } 140 141 log.Printf("[TRACE] %s: returned %d results", d, len(entries)) 142 143 list := make([]*HealthService, 0, len(entries)) 144 for _, entry := range entries { 145 // Get the status of this service from its checks. 146 status := entry.Checks.AggregatedStatus() 147 148 // If we are not checking only healthy services, filter out services that do 149 // not match the given filter. 150 if !acceptStatus(d.filters, status) { 151 continue 152 } 153 154 // Get the address of the service, falling back to the address of the node. 155 address := entry.Service.Address 156 if address == "" { 157 address = entry.Node.Address 158 } 159 160 list = append(list, &HealthService{ 161 Node: entry.Node.Node, 162 NodeID: entry.Node.ID, 163 NodeAddress: entry.Node.Address, 164 NodeTaggedAddresses: entry.Node.TaggedAddresses, 165 NodeMeta: entry.Node.Meta, 166 ServiceMeta: entry.Service.Meta, 167 Address: address, 168 ID: entry.Service.ID, 169 Name: entry.Service.Service, 170 Tags: ServiceTags(deepCopyAndSortTags(entry.Service.Tags)), 171 Status: status, 172 Checks: entry.Checks, 173 Port: entry.Service.Port, 174 }) 175 } 176 177 log.Printf("[TRACE] %s: returned %d results after filtering", d, len(list)) 178 179 // Sort unless the user explicitly asked for nearness 180 if d.near == "" { 181 sort.Stable(ByNodeThenID(list)) 182 } 183 184 rm := &ResponseMetadata{ 185 LastIndex: qm.LastIndex, 186 LastContact: qm.LastContact, 187 } 188 189 return list, rm, nil 190} 191 192// CanShare returns a boolean if this dependency is shareable. 193func (d *HealthServiceQuery) CanShare() bool { 194 return true 195} 196 197// Stop halts the dependency's fetch function. 198func (d *HealthServiceQuery) Stop() { 199 close(d.stopCh) 200} 201 202// String returns the human-friendly version of this dependency. 203func (d *HealthServiceQuery) String() string { 204 name := d.name 205 if d.tag != "" { 206 name = d.tag + "." + name 207 } 208 if d.dc != "" { 209 name = name + "@" + d.dc 210 } 211 if d.near != "" { 212 name = name + "~" + d.near 213 } 214 if len(d.filters) > 0 { 215 name = name + "|" + strings.Join(d.filters, ",") 216 } 217 return fmt.Sprintf("health.service(%s)", name) 218} 219 220// Type returns the type of this dependency. 221func (d *HealthServiceQuery) Type() Type { 222 return TypeConsul 223} 224 225// acceptStatus allows us to check if a slice of health checks pass this filter. 226func acceptStatus(list []string, s string) bool { 227 for _, status := range list { 228 if status == s || status == HealthAny { 229 return true 230 } 231 } 232 return false 233} 234 235// ByNodeThenID is a sortable slice of Service 236type ByNodeThenID []*HealthService 237 238// Len, Swap, and Less are used to implement the sort.Sort interface. 239func (s ByNodeThenID) Len() int { return len(s) } 240func (s ByNodeThenID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 241func (s ByNodeThenID) Less(i, j int) bool { 242 if s[i].Node < s[j].Node { 243 return true 244 } else if s[i].Node == s[j].Node { 245 return s[i].ID <= s[j].ID 246 } 247 return false 248} 249