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