1package api
2
3import (
4	"encoding/json"
5	"fmt"
6	"strings"
7	"time"
8)
9
10const (
11	// HealthAny is special, and is used as a wild card,
12	// not as a specific state.
13	HealthAny      = "any"
14	HealthPassing  = "passing"
15	HealthWarning  = "warning"
16	HealthCritical = "critical"
17	HealthMaint    = "maintenance"
18)
19
20const (
21	serviceHealth = "service"
22	connectHealth = "connect"
23	ingressHealth = "ingress"
24)
25
26const (
27	// NodeMaint is the special key set by a node in maintenance mode.
28	NodeMaint = "_node_maintenance"
29
30	// ServiceMaintPrefix is the prefix for a service in maintenance mode.
31	ServiceMaintPrefix = "_service_maintenance:"
32)
33
34// HealthCheck is used to represent a single check
35type HealthCheck struct {
36	Node        string
37	CheckID     string
38	Name        string
39	Status      string
40	Notes       string
41	Output      string
42	ServiceID   string
43	ServiceName string
44	ServiceTags []string
45	Type        string
46	Namespace   string `json:",omitempty"`
47
48	Definition HealthCheckDefinition
49
50	CreateIndex uint64
51	ModifyIndex uint64
52}
53
54// HealthCheckDefinition is used to store the details about
55// a health check's execution.
56type HealthCheckDefinition struct {
57	HTTP                                   string
58	Header                                 map[string][]string
59	Method                                 string
60	Body                                   string
61	TLSServerName                          string
62	TLSSkipVerify                          bool
63	TCP                                    string
64	IntervalDuration                       time.Duration `json:"-"`
65	TimeoutDuration                        time.Duration `json:"-"`
66	DeregisterCriticalServiceAfterDuration time.Duration `json:"-"`
67
68	// DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead.
69	Interval                       ReadableDuration
70	Timeout                        ReadableDuration
71	DeregisterCriticalServiceAfter ReadableDuration
72}
73
74func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) {
75	type Alias HealthCheckDefinition
76	out := &struct {
77		Interval                       string
78		Timeout                        string
79		DeregisterCriticalServiceAfter string
80		*Alias
81	}{
82		Interval:                       d.Interval.String(),
83		Timeout:                        d.Timeout.String(),
84		DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(),
85		Alias:                          (*Alias)(d),
86	}
87
88	if d.IntervalDuration != 0 {
89		out.Interval = d.IntervalDuration.String()
90	} else if d.Interval != 0 {
91		out.Interval = d.Interval.String()
92	}
93	if d.TimeoutDuration != 0 {
94		out.Timeout = d.TimeoutDuration.String()
95	} else if d.Timeout != 0 {
96		out.Timeout = d.Timeout.String()
97	}
98	if d.DeregisterCriticalServiceAfterDuration != 0 {
99		out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String()
100	} else if d.DeregisterCriticalServiceAfter != 0 {
101		out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String()
102	}
103
104	return json.Marshal(out)
105}
106
107func (t *HealthCheckDefinition) UnmarshalJSON(data []byte) (err error) {
108	type Alias HealthCheckDefinition
109	aux := &struct {
110		IntervalDuration                       interface{}
111		TimeoutDuration                        interface{}
112		DeregisterCriticalServiceAfterDuration interface{}
113		*Alias
114	}{
115		Alias: (*Alias)(t),
116	}
117	if err := json.Unmarshal(data, &aux); err != nil {
118		return err
119	}
120
121	// Parse the values into both the time.Duration and old ReadableDuration fields.
122
123	if aux.IntervalDuration == nil {
124		t.IntervalDuration = time.Duration(t.Interval)
125	} else {
126		switch v := aux.IntervalDuration.(type) {
127		case string:
128			if t.IntervalDuration, err = time.ParseDuration(v); err != nil {
129				return err
130			}
131		case float64:
132			t.IntervalDuration = time.Duration(v)
133		}
134		t.Interval = ReadableDuration(t.IntervalDuration)
135	}
136
137	if aux.TimeoutDuration == nil {
138		t.TimeoutDuration = time.Duration(t.Timeout)
139	} else {
140		switch v := aux.TimeoutDuration.(type) {
141		case string:
142			if t.TimeoutDuration, err = time.ParseDuration(v); err != nil {
143				return err
144			}
145		case float64:
146			t.TimeoutDuration = time.Duration(v)
147		}
148		t.Timeout = ReadableDuration(t.TimeoutDuration)
149	}
150	if aux.DeregisterCriticalServiceAfterDuration == nil {
151		t.DeregisterCriticalServiceAfterDuration = time.Duration(t.DeregisterCriticalServiceAfter)
152	} else {
153		switch v := aux.DeregisterCriticalServiceAfterDuration.(type) {
154		case string:
155			if t.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(v); err != nil {
156				return err
157			}
158		case float64:
159			t.DeregisterCriticalServiceAfterDuration = time.Duration(v)
160		}
161		t.DeregisterCriticalServiceAfter = ReadableDuration(t.DeregisterCriticalServiceAfterDuration)
162	}
163
164	return nil
165}
166
167// HealthChecks is a collection of HealthCheck structs.
168type HealthChecks []*HealthCheck
169
170// AggregatedStatus returns the "best" status for the list of health checks.
171// Because a given entry may have many service and node-level health checks
172// attached, this function determines the best representative of the status as
173// as single string using the following heuristic:
174//
175//  maintenance > critical > warning > passing
176//
177func (c HealthChecks) AggregatedStatus() string {
178	var passing, warning, critical, maintenance bool
179	for _, check := range c {
180		id := check.CheckID
181		if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) {
182			maintenance = true
183			continue
184		}
185
186		switch check.Status {
187		case HealthPassing:
188			passing = true
189		case HealthWarning:
190			warning = true
191		case HealthCritical:
192			critical = true
193		default:
194			return ""
195		}
196	}
197
198	switch {
199	case maintenance:
200		return HealthMaint
201	case critical:
202		return HealthCritical
203	case warning:
204		return HealthWarning
205	case passing:
206		return HealthPassing
207	default:
208		return HealthPassing
209	}
210}
211
212// ServiceEntry is used for the health service endpoint
213type ServiceEntry struct {
214	Node    *Node
215	Service *AgentService
216	Checks  HealthChecks
217}
218
219// Health can be used to query the Health endpoints
220type Health struct {
221	c *Client
222}
223
224// Health returns a handle to the health endpoints
225func (c *Client) Health() *Health {
226	return &Health{c}
227}
228
229// Node is used to query for checks belonging to a given node
230func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
231	r := h.c.newRequest("GET", "/v1/health/node/"+node)
232	r.setQueryOptions(q)
233	rtt, resp, err := requireOK(h.c.doRequest(r))
234	if err != nil {
235		return nil, nil, err
236	}
237	defer closeResponseBody(resp)
238
239	qm := &QueryMeta{}
240	parseQueryMeta(resp, qm)
241	qm.RequestTime = rtt
242
243	var out HealthChecks
244	if err := decodeBody(resp, &out); err != nil {
245		return nil, nil, err
246	}
247	return out, qm, nil
248}
249
250// Checks is used to return the checks associated with a service
251func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
252	r := h.c.newRequest("GET", "/v1/health/checks/"+service)
253	r.setQueryOptions(q)
254	rtt, resp, err := requireOK(h.c.doRequest(r))
255	if err != nil {
256		return nil, nil, err
257	}
258	defer closeResponseBody(resp)
259
260	qm := &QueryMeta{}
261	parseQueryMeta(resp, qm)
262	qm.RequestTime = rtt
263
264	var out HealthChecks
265	if err := decodeBody(resp, &out); err != nil {
266		return nil, nil, err
267	}
268	return out, qm, nil
269}
270
271// Service is used to query health information along with service info
272// for a given service. It can optionally do server-side filtering on a tag
273// or nodes with passing health checks only.
274func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
275	var tags []string
276	if tag != "" {
277		tags = []string{tag}
278	}
279	return h.service(service, tags, passingOnly, q, serviceHealth)
280}
281
282func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
283	return h.service(service, tags, passingOnly, q, serviceHealth)
284}
285
286// Connect is equivalent to Service except that it will only return services
287// which are Connect-enabled and will returns the connection address for Connect
288// client's to use which may be a proxy in front of the named service. If
289// passingOnly is true only instances where both the service and any proxy are
290// healthy will be returned.
291func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
292	var tags []string
293	if tag != "" {
294		tags = []string{tag}
295	}
296	return h.service(service, tags, passingOnly, q, connectHealth)
297}
298
299func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
300	return h.service(service, tags, passingOnly, q, connectHealth)
301}
302
303// Ingress is equivalent to Connect except that it will only return associated
304// ingress gateways for the requested service.
305func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) {
306	var tags []string
307	return h.service(service, tags, passingOnly, q, ingressHealth)
308}
309
310func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, healthType string) ([]*ServiceEntry, *QueryMeta, error) {
311	var path string
312	switch healthType {
313	case connectHealth:
314		path = "/v1/health/connect/" + service
315	case ingressHealth:
316		path = "/v1/health/ingress/" + service
317	default:
318		path = "/v1/health/service/" + service
319	}
320
321	r := h.c.newRequest("GET", path)
322	r.setQueryOptions(q)
323	if len(tags) > 0 {
324		for _, tag := range tags {
325			r.params.Add("tag", tag)
326		}
327	}
328	if passingOnly {
329		r.params.Set(HealthPassing, "1")
330	}
331	rtt, resp, err := requireOK(h.c.doRequest(r))
332	if err != nil {
333		return nil, nil, err
334	}
335	defer closeResponseBody(resp)
336
337	qm := &QueryMeta{}
338	parseQueryMeta(resp, qm)
339	qm.RequestTime = rtt
340
341	var out []*ServiceEntry
342	if err := decodeBody(resp, &out); err != nil {
343		return nil, nil, err
344	}
345	return out, qm, nil
346}
347
348// State is used to retrieve all the checks in a given state.
349// The wildcard "any" state can also be used for all checks.
350func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) {
351	switch state {
352	case HealthAny:
353	case HealthWarning:
354	case HealthCritical:
355	case HealthPassing:
356	default:
357		return nil, nil, fmt.Errorf("Unsupported state: %v", state)
358	}
359	r := h.c.newRequest("GET", "/v1/health/state/"+state)
360	r.setQueryOptions(q)
361	rtt, resp, err := requireOK(h.c.doRequest(r))
362	if err != nil {
363		return nil, nil, err
364	}
365	defer closeResponseBody(resp)
366
367	qm := &QueryMeta{}
368	parseQueryMeta(resp, qm)
369	qm.RequestTime = rtt
370
371	var out HealthChecks
372	if err := decodeBody(resp, &out); err != nil {
373		return nil, nil, err
374	}
375	return out, qm, nil
376}
377