mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
11 KiB
398 lines
11 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package api |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"strings" |
|
"time" |
|
) |
|
|
|
const ( |
|
// HealthAny is special, and is used as a wild card, |
|
// not as a specific state. |
|
HealthAny = "any" |
|
HealthPassing = "passing" |
|
HealthWarning = "warning" |
|
HealthCritical = "critical" |
|
HealthMaint = "maintenance" |
|
) |
|
|
|
const ( |
|
serviceHealth = "service" |
|
connectHealth = "connect" |
|
ingressHealth = "ingress" |
|
) |
|
|
|
const ( |
|
// NodeMaint is the special key set by a node in maintenance mode. |
|
NodeMaint = "_node_maintenance" |
|
|
|
// ServiceMaintPrefix is the prefix for a service in maintenance mode. |
|
ServiceMaintPrefix = "_service_maintenance:" |
|
) |
|
|
|
// HealthCheck is used to represent a single check |
|
type HealthCheck struct { |
|
Node string |
|
CheckID string |
|
Name string |
|
Status string |
|
Notes string |
|
Output string |
|
ServiceID string |
|
ServiceName string |
|
ServiceTags []string |
|
Type string |
|
Namespace string `json:",omitempty"` |
|
Partition string `json:",omitempty"` |
|
ExposedPort int |
|
PeerName string `json:",omitempty"` |
|
|
|
Definition HealthCheckDefinition |
|
|
|
CreateIndex uint64 |
|
ModifyIndex uint64 |
|
} |
|
|
|
// HealthCheckDefinition is used to store the details about |
|
// a health check's execution. |
|
type HealthCheckDefinition struct { |
|
HTTP string |
|
Header map[string][]string |
|
Method string |
|
Body string |
|
TLSServerName string |
|
TLSSkipVerify bool |
|
TCP string |
|
TCPUseTLS bool |
|
UDP string |
|
GRPC string |
|
OSService string |
|
GRPCUseTLS bool |
|
IntervalDuration time.Duration `json:"-"` |
|
TimeoutDuration time.Duration `json:"-"` |
|
DeregisterCriticalServiceAfterDuration time.Duration `json:"-"` |
|
|
|
// DEPRECATED in Consul 1.4.1. Use the above time.Duration fields instead. |
|
Interval ReadableDuration |
|
Timeout ReadableDuration |
|
DeregisterCriticalServiceAfter ReadableDuration |
|
} |
|
|
|
func (d *HealthCheckDefinition) MarshalJSON() ([]byte, error) { |
|
type Alias HealthCheckDefinition |
|
out := &struct { |
|
Interval string |
|
Timeout string |
|
DeregisterCriticalServiceAfter string |
|
*Alias |
|
}{ |
|
Interval: d.Interval.String(), |
|
Timeout: d.Timeout.String(), |
|
DeregisterCriticalServiceAfter: d.DeregisterCriticalServiceAfter.String(), |
|
Alias: (*Alias)(d), |
|
} |
|
|
|
if d.IntervalDuration != 0 { |
|
out.Interval = d.IntervalDuration.String() |
|
} else if d.Interval != 0 { |
|
out.Interval = d.Interval.String() |
|
} |
|
if d.TimeoutDuration != 0 { |
|
out.Timeout = d.TimeoutDuration.String() |
|
} else if d.Timeout != 0 { |
|
out.Timeout = d.Timeout.String() |
|
} |
|
if d.DeregisterCriticalServiceAfterDuration != 0 { |
|
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfterDuration.String() |
|
} else if d.DeregisterCriticalServiceAfter != 0 { |
|
out.DeregisterCriticalServiceAfter = d.DeregisterCriticalServiceAfter.String() |
|
} |
|
|
|
return json.Marshal(out) |
|
} |
|
|
|
func (t *HealthCheckDefinition) UnmarshalJSON(data []byte) (err error) { |
|
type Alias HealthCheckDefinition |
|
aux := &struct { |
|
IntervalDuration interface{} |
|
TimeoutDuration interface{} |
|
DeregisterCriticalServiceAfterDuration interface{} |
|
*Alias |
|
}{ |
|
Alias: (*Alias)(t), |
|
} |
|
if err := json.Unmarshal(data, &aux); err != nil { |
|
return err |
|
} |
|
|
|
// Parse the values into both the time.Duration and old ReadableDuration fields. |
|
|
|
if aux.IntervalDuration == nil { |
|
t.IntervalDuration = time.Duration(t.Interval) |
|
} else { |
|
switch v := aux.IntervalDuration.(type) { |
|
case string: |
|
if t.IntervalDuration, err = time.ParseDuration(v); err != nil { |
|
return err |
|
} |
|
case float64: |
|
t.IntervalDuration = time.Duration(v) |
|
} |
|
t.Interval = ReadableDuration(t.IntervalDuration) |
|
} |
|
|
|
if aux.TimeoutDuration == nil { |
|
t.TimeoutDuration = time.Duration(t.Timeout) |
|
} else { |
|
switch v := aux.TimeoutDuration.(type) { |
|
case string: |
|
if t.TimeoutDuration, err = time.ParseDuration(v); err != nil { |
|
return err |
|
} |
|
case float64: |
|
t.TimeoutDuration = time.Duration(v) |
|
} |
|
t.Timeout = ReadableDuration(t.TimeoutDuration) |
|
} |
|
if aux.DeregisterCriticalServiceAfterDuration == nil { |
|
t.DeregisterCriticalServiceAfterDuration = time.Duration(t.DeregisterCriticalServiceAfter) |
|
} else { |
|
switch v := aux.DeregisterCriticalServiceAfterDuration.(type) { |
|
case string: |
|
if t.DeregisterCriticalServiceAfterDuration, err = time.ParseDuration(v); err != nil { |
|
return err |
|
} |
|
case float64: |
|
t.DeregisterCriticalServiceAfterDuration = time.Duration(v) |
|
} |
|
t.DeregisterCriticalServiceAfter = ReadableDuration(t.DeregisterCriticalServiceAfterDuration) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// HealthChecks is a collection of HealthCheck structs. |
|
type HealthChecks []*HealthCheck |
|
|
|
// AggregatedStatus returns the "best" status for the list of health checks. |
|
// Because a given entry may have many service and node-level health checks |
|
// attached, this function determines the best representative of the status as |
|
// as single string using the following heuristic: |
|
// |
|
// maintenance > critical > warning > passing |
|
func (c HealthChecks) AggregatedStatus() string { |
|
var passing, warning, critical, maintenance bool |
|
for _, check := range c { |
|
id := check.CheckID |
|
if id == NodeMaint || strings.HasPrefix(id, ServiceMaintPrefix) { |
|
maintenance = true |
|
continue |
|
} |
|
|
|
switch check.Status { |
|
case HealthPassing: |
|
passing = true |
|
case HealthWarning: |
|
warning = true |
|
case HealthCritical: |
|
critical = true |
|
default: |
|
return "" |
|
} |
|
} |
|
|
|
switch { |
|
case maintenance: |
|
return HealthMaint |
|
case critical: |
|
return HealthCritical |
|
case warning: |
|
return HealthWarning |
|
case passing: |
|
return HealthPassing |
|
default: |
|
return HealthPassing |
|
} |
|
} |
|
|
|
// ServiceEntry is used for the health service endpoint |
|
type ServiceEntry struct { |
|
Node *Node |
|
Service *AgentService |
|
Checks HealthChecks |
|
} |
|
|
|
// Health can be used to query the Health endpoints |
|
type Health struct { |
|
c *Client |
|
} |
|
|
|
// Health returns a handle to the health endpoints |
|
func (c *Client) Health() *Health { |
|
return &Health{c} |
|
} |
|
|
|
// Node is used to query for checks belonging to a given node |
|
func (h *Health) Node(node string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/health/node/"+node) |
|
r.setQueryOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out HealthChecks |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
} |
|
|
|
// Checks is used to return the checks associated with a service |
|
func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/health/checks/"+service) |
|
r.setQueryOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out HealthChecks |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
} |
|
|
|
// Service is used to query health information along with service info |
|
// for a given service. It can optionally do server-side filtering on a tag |
|
// or nodes with passing health checks only. |
|
func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
|
var tags []string |
|
if tag != "" { |
|
tags = []string{tag} |
|
} |
|
return h.service(service, tags, passingOnly, q, serviceHealth) |
|
} |
|
|
|
func (h *Health) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
|
return h.service(service, tags, passingOnly, q, serviceHealth) |
|
} |
|
|
|
// Connect is equivalent to Service except that it will only return services |
|
// which are Connect-enabled and will returns the connection address for Connect |
|
// client's to use which may be a proxy in front of the named service. If |
|
// passingOnly is true only instances where both the service and any proxy are |
|
// healthy will be returned. |
|
func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
|
var tags []string |
|
if tag != "" { |
|
tags = []string{tag} |
|
} |
|
return h.service(service, tags, passingOnly, q, connectHealth) |
|
} |
|
|
|
func (h *Health) ConnectMultipleTags(service string, tags []string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
|
return h.service(service, tags, passingOnly, q, connectHealth) |
|
} |
|
|
|
// Ingress is equivalent to Connect except that it will only return associated |
|
// ingress gateways for the requested service. |
|
func (h *Health) Ingress(service string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { |
|
var tags []string |
|
return h.service(service, tags, passingOnly, q, ingressHealth) |
|
} |
|
|
|
func (h *Health) service(service string, tags []string, passingOnly bool, q *QueryOptions, healthType string) ([]*ServiceEntry, *QueryMeta, error) { |
|
var path string |
|
switch healthType { |
|
case connectHealth: |
|
path = "/v1/health/connect/" + service |
|
case ingressHealth: |
|
path = "/v1/health/ingress/" + service |
|
default: |
|
path = "/v1/health/service/" + service |
|
} |
|
|
|
r := h.c.newRequest("GET", path) |
|
r.setQueryOptions(q) |
|
if len(tags) > 0 { |
|
for _, tag := range tags { |
|
r.params.Add("tag", tag) |
|
} |
|
} |
|
if passingOnly { |
|
r.params.Set(HealthPassing, "1") |
|
} |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out []*ServiceEntry |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
} |
|
|
|
// State is used to retrieve all the checks in a given state. |
|
// The wildcard "any" state can also be used for all checks. |
|
func (h *Health) State(state string, q *QueryOptions) (HealthChecks, *QueryMeta, error) { |
|
switch state { |
|
case HealthAny: |
|
case HealthWarning: |
|
case HealthCritical: |
|
case HealthPassing: |
|
default: |
|
return nil, nil, fmt.Errorf("Unsupported state: %v", state) |
|
} |
|
r := h.c.newRequest("GET", "/v1/health/state/"+state) |
|
r.setQueryOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out HealthChecks |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
}
|
|
|