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