blob: 11c3a2cd7cdcc6dc797d7dd0b75f0f8c8f724a97 [file] [log] [blame]
William Kurkianea869482019-04-09 15:16:11 -04001package api
2
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
7 "encoding/json"
8 "fmt"
9 "io"
10 "io/ioutil"
11 "log"
12 "net"
13 "net/http"
14 "net/url"
15 "os"
16 "strconv"
17 "strings"
18 "time"
19
20 "github.com/hashicorp/go-cleanhttp"
21 "github.com/hashicorp/go-rootcerts"
22)
23
24const (
25 // HTTPAddrEnvName defines an environment variable name which sets
26 // the HTTP address if there is no -http-addr specified.
27 HTTPAddrEnvName = "CONSUL_HTTP_ADDR"
28
29 // HTTPTokenEnvName defines an environment variable name which sets
30 // the HTTP token.
31 HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"
32
Abhilash S.L3b494632019-07-16 15:51:09 +053033 // HTTPTokenFileEnvName defines an environment variable name which sets
34 // the HTTP token file.
35 HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE"
36
William Kurkianea869482019-04-09 15:16:11 -040037 // HTTPAuthEnvName defines an environment variable name which sets
38 // the HTTP authentication header.
39 HTTPAuthEnvName = "CONSUL_HTTP_AUTH"
40
41 // HTTPSSLEnvName defines an environment variable name which sets
42 // whether or not to use HTTPS.
43 HTTPSSLEnvName = "CONSUL_HTTP_SSL"
44
45 // HTTPCAFile defines an environment variable name which sets the
46 // CA file to use for talking to Consul over TLS.
47 HTTPCAFile = "CONSUL_CACERT"
48
49 // HTTPCAPath defines an environment variable name which sets the
50 // path to a directory of CA certs to use for talking to Consul over TLS.
51 HTTPCAPath = "CONSUL_CAPATH"
52
53 // HTTPClientCert defines an environment variable name which sets the
54 // client cert file to use for talking to Consul over TLS.
55 HTTPClientCert = "CONSUL_CLIENT_CERT"
56
57 // HTTPClientKey defines an environment variable name which sets the
58 // client key file to use for talking to Consul over TLS.
59 HTTPClientKey = "CONSUL_CLIENT_KEY"
60
61 // HTTPTLSServerName defines an environment variable name which sets the
62 // server name to use as the SNI host when connecting via TLS
63 HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME"
64
65 // HTTPSSLVerifyEnvName defines an environment variable name which sets
66 // whether or not to disable certificate checking.
67 HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"
68
69 // GRPCAddrEnvName defines an environment variable name which sets the gRPC
70 // address for consul connect envoy. Note this isn't actually used by the api
71 // client in this package but is defined here for consistency with all the
72 // other ENV names we use.
73 GRPCAddrEnvName = "CONSUL_GRPC_ADDR"
74)
75
76// QueryOptions are used to parameterize a query
77type QueryOptions struct {
78 // Providing a datacenter overwrites the DC provided
79 // by the Config
80 Datacenter string
81
82 // AllowStale allows any Consul server (non-leader) to service
83 // a read. This allows for lower latency and higher throughput
84 AllowStale bool
85
86 // RequireConsistent forces the read to be fully consistent.
87 // This is more expensive but prevents ever performing a stale
88 // read.
89 RequireConsistent bool
90
91 // UseCache requests that the agent cache results locally. See
Abhilash S.L3b494632019-07-16 15:51:09 +053092 // https://www.consul.io/api/features/caching.html for more details on the
William Kurkianea869482019-04-09 15:16:11 -040093 // semantics.
94 UseCache bool
95
96 // MaxAge limits how old a cached value will be returned if UseCache is true.
97 // If there is a cached response that is older than the MaxAge, it is treated
98 // as a cache miss and a new fetch invoked. If the fetch fails, the error is
99 // returned. Clients that wish to allow for stale results on error can set
100 // StaleIfError to a longer duration to change this behavior. It is ignored
101 // if the endpoint supports background refresh caching. See
Abhilash S.L3b494632019-07-16 15:51:09 +0530102 // https://www.consul.io/api/features/caching.html for more details.
William Kurkianea869482019-04-09 15:16:11 -0400103 MaxAge time.Duration
104
105 // StaleIfError specifies how stale the client will accept a cached response
106 // if the servers are unavailable to fetch a fresh one. Only makes sense when
107 // UseCache is true and MaxAge is set to a lower, non-zero value. It is
108 // ignored if the endpoint supports background refresh caching. See
Abhilash S.L3b494632019-07-16 15:51:09 +0530109 // https://www.consul.io/api/features/caching.html for more details.
William Kurkianea869482019-04-09 15:16:11 -0400110 StaleIfError time.Duration
111
112 // WaitIndex is used to enable a blocking query. Waits
113 // until the timeout or the next index is reached
114 WaitIndex uint64
115
116 // WaitHash is used by some endpoints instead of WaitIndex to perform blocking
117 // on state based on a hash of the response rather than a monotonic index.
118 // This is required when the state being blocked on is not stored in Raft, for
119 // example agent-local proxy configuration.
120 WaitHash string
121
122 // WaitTime is used to bound the duration of a wait.
123 // Defaults to that of the Config, but can be overridden.
124 WaitTime time.Duration
125
126 // Token is used to provide a per-request ACL token
127 // which overrides the agent's default token.
128 Token string
129
130 // Near is used to provide a node name that will sort the results
131 // in ascending order based on the estimated round trip time from
132 // that node. Setting this to "_agent" will use the agent's node
133 // for the sort.
134 Near string
135
136 // NodeMeta is used to filter results by nodes with the given
137 // metadata key/value pairs. Currently, only one key/value pair can
138 // be provided for filtering.
139 NodeMeta map[string]string
140
141 // RelayFactor is used in keyring operations to cause responses to be
142 // relayed back to the sender through N other random nodes. Must be
143 // a value from 0 to 5 (inclusive).
144 RelayFactor uint8
145
146 // Connect filters prepared query execution to only include Connect-capable
147 // services. This currently affects prepared query execution.
148 Connect bool
149
150 // ctx is an optional context pass through to the underlying HTTP
151 // request layer. Use Context() and WithContext() to manage this.
152 ctx context.Context
Abhilash S.L3b494632019-07-16 15:51:09 +0530153
154 // Filter requests filtering data prior to it being returned. The string
155 // is a go-bexpr compatible expression.
156 Filter string
William Kurkianea869482019-04-09 15:16:11 -0400157}
158
159func (o *QueryOptions) Context() context.Context {
160 if o != nil && o.ctx != nil {
161 return o.ctx
162 }
163 return context.Background()
164}
165
166func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions {
167 o2 := new(QueryOptions)
168 if o != nil {
169 *o2 = *o
170 }
171 o2.ctx = ctx
172 return o2
173}
174
175// WriteOptions are used to parameterize a write
176type WriteOptions struct {
177 // Providing a datacenter overwrites the DC provided
178 // by the Config
179 Datacenter string
180
181 // Token is used to provide a per-request ACL token
182 // which overrides the agent's default token.
183 Token string
184
185 // RelayFactor is used in keyring operations to cause responses to be
186 // relayed back to the sender through N other random nodes. Must be
187 // a value from 0 to 5 (inclusive).
188 RelayFactor uint8
189
190 // ctx is an optional context pass through to the underlying HTTP
191 // request layer. Use Context() and WithContext() to manage this.
192 ctx context.Context
193}
194
195func (o *WriteOptions) Context() context.Context {
196 if o != nil && o.ctx != nil {
197 return o.ctx
198 }
199 return context.Background()
200}
201
202func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions {
203 o2 := new(WriteOptions)
204 if o != nil {
205 *o2 = *o
206 }
207 o2.ctx = ctx
208 return o2
209}
210
211// QueryMeta is used to return meta data about a query
212type QueryMeta struct {
213 // LastIndex. This can be used as a WaitIndex to perform
214 // a blocking query
215 LastIndex uint64
216
217 // LastContentHash. This can be used as a WaitHash to perform a blocking query
218 // for endpoints that support hash-based blocking. Endpoints that do not
219 // support it will return an empty hash.
220 LastContentHash string
221
222 // Time of last contact from the leader for the
223 // server servicing the request
224 LastContact time.Duration
225
226 // Is there a known leader
227 KnownLeader bool
228
229 // How long did the request take
230 RequestTime time.Duration
231
232 // Is address translation enabled for HTTP responses on this agent
233 AddressTranslationEnabled bool
234
235 // CacheHit is true if the result was served from agent-local cache.
236 CacheHit bool
237
238 // CacheAge is set if request was ?cached and indicates how stale the cached
239 // response is.
240 CacheAge time.Duration
241}
242
243// WriteMeta is used to return meta data about a write
244type WriteMeta struct {
245 // How long did the request take
246 RequestTime time.Duration
247}
248
249// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication
250type HttpBasicAuth struct {
251 // Username to use for HTTP Basic Authentication
252 Username string
253
254 // Password to use for HTTP Basic Authentication
255 Password string
256}
257
258// Config is used to configure the creation of a client
259type Config struct {
260 // Address is the address of the Consul server
261 Address string
262
263 // Scheme is the URI scheme for the Consul server
264 Scheme string
265
266 // Datacenter to use. If not provided, the default agent datacenter is used.
267 Datacenter string
268
269 // Transport is the Transport to use for the http client.
270 Transport *http.Transport
271
272 // HttpClient is the client to use. Default will be
273 // used if not provided.
274 HttpClient *http.Client
275
276 // HttpAuth is the auth info to use for http access.
277 HttpAuth *HttpBasicAuth
278
279 // WaitTime limits how long a Watch will block. If not provided,
280 // the agent default values will be used.
281 WaitTime time.Duration
282
283 // Token is used to provide a per-request ACL token
284 // which overrides the agent's default token.
285 Token string
286
Abhilash S.L3b494632019-07-16 15:51:09 +0530287 // TokenFile is a file containing the current token to use for this client.
288 // If provided it is read once at startup and never again.
289 TokenFile string
290
William Kurkianea869482019-04-09 15:16:11 -0400291 TLSConfig TLSConfig
292}
293
294// TLSConfig is used to generate a TLSClientConfig that's useful for talking to
295// Consul using TLS.
296type TLSConfig struct {
297 // Address is the optional address of the Consul server. The port, if any
298 // will be removed from here and this will be set to the ServerName of the
299 // resulting config.
300 Address string
301
302 // CAFile is the optional path to the CA certificate used for Consul
303 // communication, defaults to the system bundle if not specified.
304 CAFile string
305
306 // CAPath is the optional path to a directory of CA certificates to use for
307 // Consul communication, defaults to the system bundle if not specified.
308 CAPath string
309
310 // CertFile is the optional path to the certificate for Consul
311 // communication. If this is set then you need to also set KeyFile.
312 CertFile string
313
314 // KeyFile is the optional path to the private key for Consul communication.
315 // If this is set then you need to also set CertFile.
316 KeyFile string
317
318 // InsecureSkipVerify if set to true will disable TLS host verification.
319 InsecureSkipVerify bool
320}
321
322// DefaultConfig returns a default configuration for the client. By default this
323// will pool and reuse idle connections to Consul. If you have a long-lived
324// client object, this is the desired behavior and should make the most efficient
325// use of the connections to Consul. If you don't reuse a client object, which
326// is not recommended, then you may notice idle connections building up over
327// time. To avoid this, use the DefaultNonPooledConfig() instead.
328func DefaultConfig() *Config {
329 return defaultConfig(cleanhttp.DefaultPooledTransport)
330}
331
332// DefaultNonPooledConfig returns a default configuration for the client which
333// does not pool connections. This isn't a recommended configuration because it
334// will reconnect to Consul on every request, but this is useful to avoid the
335// accumulation of idle connections if you make many client objects during the
336// lifetime of your application.
337func DefaultNonPooledConfig() *Config {
338 return defaultConfig(cleanhttp.DefaultTransport)
339}
340
341// defaultConfig returns the default configuration for the client, using the
342// given function to make the transport.
343func defaultConfig(transportFn func() *http.Transport) *Config {
344 config := &Config{
345 Address: "127.0.0.1:8500",
346 Scheme: "http",
347 Transport: transportFn(),
348 }
349
350 if addr := os.Getenv(HTTPAddrEnvName); addr != "" {
351 config.Address = addr
352 }
353
Abhilash S.L3b494632019-07-16 15:51:09 +0530354 if tokenFile := os.Getenv(HTTPTokenFileEnvName); tokenFile != "" {
355 config.TokenFile = tokenFile
356 }
357
William Kurkianea869482019-04-09 15:16:11 -0400358 if token := os.Getenv(HTTPTokenEnvName); token != "" {
359 config.Token = token
360 }
361
362 if auth := os.Getenv(HTTPAuthEnvName); auth != "" {
363 var username, password string
364 if strings.Contains(auth, ":") {
365 split := strings.SplitN(auth, ":", 2)
366 username = split[0]
367 password = split[1]
368 } else {
369 username = auth
370 }
371
372 config.HttpAuth = &HttpBasicAuth{
373 Username: username,
374 Password: password,
375 }
376 }
377
378 if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" {
379 enabled, err := strconv.ParseBool(ssl)
380 if err != nil {
381 log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLEnvName, err)
382 }
383
384 if enabled {
385 config.Scheme = "https"
386 }
387 }
388
389 if v := os.Getenv(HTTPTLSServerName); v != "" {
390 config.TLSConfig.Address = v
391 }
392 if v := os.Getenv(HTTPCAFile); v != "" {
393 config.TLSConfig.CAFile = v
394 }
395 if v := os.Getenv(HTTPCAPath); v != "" {
396 config.TLSConfig.CAPath = v
397 }
398 if v := os.Getenv(HTTPClientCert); v != "" {
399 config.TLSConfig.CertFile = v
400 }
401 if v := os.Getenv(HTTPClientKey); v != "" {
402 config.TLSConfig.KeyFile = v
403 }
404 if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" {
405 doVerify, err := strconv.ParseBool(v)
406 if err != nil {
407 log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err)
408 }
409 if !doVerify {
410 config.TLSConfig.InsecureSkipVerify = true
411 }
412 }
413
414 return config
415}
416
417// TLSConfig is used to generate a TLSClientConfig that's useful for talking to
418// Consul using TLS.
419func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) {
420 tlsClientConfig := &tls.Config{
421 InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
422 }
423
424 if tlsConfig.Address != "" {
425 server := tlsConfig.Address
426 hasPort := strings.LastIndex(server, ":") > strings.LastIndex(server, "]")
427 if hasPort {
428 var err error
429 server, _, err = net.SplitHostPort(server)
430 if err != nil {
431 return nil, err
432 }
433 }
434 tlsClientConfig.ServerName = server
435 }
436
437 if tlsConfig.CertFile != "" && tlsConfig.KeyFile != "" {
438 tlsCert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile)
439 if err != nil {
440 return nil, err
441 }
442 tlsClientConfig.Certificates = []tls.Certificate{tlsCert}
443 }
444
445 if tlsConfig.CAFile != "" || tlsConfig.CAPath != "" {
446 rootConfig := &rootcerts.Config{
447 CAFile: tlsConfig.CAFile,
448 CAPath: tlsConfig.CAPath,
449 }
450 if err := rootcerts.ConfigureTLS(tlsClientConfig, rootConfig); err != nil {
451 return nil, err
452 }
453 }
454
455 return tlsClientConfig, nil
456}
457
458func (c *Config) GenerateEnv() []string {
459 env := make([]string, 0, 10)
460
461 env = append(env,
462 fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address),
463 fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token),
Abhilash S.L3b494632019-07-16 15:51:09 +0530464 fmt.Sprintf("%s=%s", HTTPTokenFileEnvName, c.TokenFile),
William Kurkianea869482019-04-09 15:16:11 -0400465 fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"),
466 fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile),
467 fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath),
468 fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile),
469 fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile),
470 fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address),
471 fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify))
472
473 if c.HttpAuth != nil {
474 env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password))
475 } else {
476 env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName))
477 }
478
479 return env
480}
481
482// Client provides a client to the Consul API
483type Client struct {
484 config Config
485}
486
487// NewClient returns a new client
488func NewClient(config *Config) (*Client, error) {
489 // bootstrap the config
490 defConfig := DefaultConfig()
491
492 if len(config.Address) == 0 {
493 config.Address = defConfig.Address
494 }
495
496 if len(config.Scheme) == 0 {
497 config.Scheme = defConfig.Scheme
498 }
499
500 if config.Transport == nil {
501 config.Transport = defConfig.Transport
502 }
503
504 if config.TLSConfig.Address == "" {
505 config.TLSConfig.Address = defConfig.TLSConfig.Address
506 }
507
508 if config.TLSConfig.CAFile == "" {
509 config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile
510 }
511
512 if config.TLSConfig.CAPath == "" {
513 config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath
514 }
515
516 if config.TLSConfig.CertFile == "" {
517 config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile
518 }
519
520 if config.TLSConfig.KeyFile == "" {
521 config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile
522 }
523
524 if !config.TLSConfig.InsecureSkipVerify {
525 config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify
526 }
527
528 if config.HttpClient == nil {
529 var err error
530 config.HttpClient, err = NewHttpClient(config.Transport, config.TLSConfig)
531 if err != nil {
532 return nil, err
533 }
534 }
535
536 parts := strings.SplitN(config.Address, "://", 2)
537 if len(parts) == 2 {
538 switch parts[0] {
539 case "http":
540 config.Scheme = "http"
541 case "https":
542 config.Scheme = "https"
543 case "unix":
544 trans := cleanhttp.DefaultTransport()
545 trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
546 return net.Dial("unix", parts[1])
547 }
548 config.HttpClient = &http.Client{
549 Transport: trans,
550 }
551 default:
552 return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
553 }
554 config.Address = parts[1]
555 }
556
Abhilash S.L3b494632019-07-16 15:51:09 +0530557 // If the TokenFile is set, always use that, even if a Token is configured.
558 // This is because when TokenFile is set it is read into the Token field.
559 // We want any derived clients to have to re-read the token file.
560 if config.TokenFile != "" {
561 data, err := ioutil.ReadFile(config.TokenFile)
562 if err != nil {
563 return nil, fmt.Errorf("Error loading token file: %s", err)
564 }
565
566 if token := strings.TrimSpace(string(data)); token != "" {
567 config.Token = token
568 }
569 }
William Kurkianea869482019-04-09 15:16:11 -0400570 if config.Token == "" {
571 config.Token = defConfig.Token
572 }
573
574 return &Client{config: *config}, nil
575}
576
577// NewHttpClient returns an http client configured with the given Transport and TLS
578// config.
579func NewHttpClient(transport *http.Transport, tlsConf TLSConfig) (*http.Client, error) {
580 client := &http.Client{
581 Transport: transport,
582 }
583
584 // TODO (slackpad) - Once we get some run time on the HTTP/2 support we
585 // should turn it on by default if TLS is enabled. We would basically
586 // just need to call http2.ConfigureTransport(transport) here. We also
587 // don't want to introduce another external dependency on
588 // golang.org/x/net/http2 at this time. For a complete recipe for how
589 // to enable HTTP/2 support on a transport suitable for the API client
590 // library see agent/http_test.go:TestHTTPServer_H2.
591
592 if transport.TLSClientConfig == nil {
593 tlsClientConfig, err := SetupTLSConfig(&tlsConf)
594
595 if err != nil {
596 return nil, err
597 }
598
599 transport.TLSClientConfig = tlsClientConfig
600 }
601
602 return client, nil
603}
604
605// request is used to help build up a request
606type request struct {
607 config *Config
608 method string
609 url *url.URL
610 params url.Values
611 body io.Reader
612 header http.Header
613 obj interface{}
614 ctx context.Context
615}
616
617// setQueryOptions is used to annotate the request with
618// additional query options
619func (r *request) setQueryOptions(q *QueryOptions) {
620 if q == nil {
621 return
622 }
623 if q.Datacenter != "" {
624 r.params.Set("dc", q.Datacenter)
625 }
626 if q.AllowStale {
627 r.params.Set("stale", "")
628 }
629 if q.RequireConsistent {
630 r.params.Set("consistent", "")
631 }
632 if q.WaitIndex != 0 {
633 r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
634 }
635 if q.WaitTime != 0 {
636 r.params.Set("wait", durToMsec(q.WaitTime))
637 }
638 if q.WaitHash != "" {
639 r.params.Set("hash", q.WaitHash)
640 }
641 if q.Token != "" {
642 r.header.Set("X-Consul-Token", q.Token)
643 }
644 if q.Near != "" {
645 r.params.Set("near", q.Near)
646 }
Abhilash S.L3b494632019-07-16 15:51:09 +0530647 if q.Filter != "" {
648 r.params.Set("filter", q.Filter)
649 }
William Kurkianea869482019-04-09 15:16:11 -0400650 if len(q.NodeMeta) > 0 {
651 for key, value := range q.NodeMeta {
652 r.params.Add("node-meta", key+":"+value)
653 }
654 }
655 if q.RelayFactor != 0 {
656 r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
657 }
658 if q.Connect {
659 r.params.Set("connect", "true")
660 }
661 if q.UseCache && !q.RequireConsistent {
662 r.params.Set("cached", "")
663
664 cc := []string{}
665 if q.MaxAge > 0 {
666 cc = append(cc, fmt.Sprintf("max-age=%.0f", q.MaxAge.Seconds()))
667 }
668 if q.StaleIfError > 0 {
669 cc = append(cc, fmt.Sprintf("stale-if-error=%.0f", q.StaleIfError.Seconds()))
670 }
671 if len(cc) > 0 {
672 r.header.Set("Cache-Control", strings.Join(cc, ", "))
673 }
674 }
675 r.ctx = q.ctx
676}
677
678// durToMsec converts a duration to a millisecond specified string. If the
679// user selected a positive value that rounds to 0 ms, then we will use 1 ms
680// so they get a short delay, otherwise Consul will translate the 0 ms into
681// a huge default delay.
682func durToMsec(dur time.Duration) string {
683 ms := dur / time.Millisecond
684 if dur > 0 && ms == 0 {
685 ms = 1
686 }
687 return fmt.Sprintf("%dms", ms)
688}
689
690// serverError is a string we look for to detect 500 errors.
691const serverError = "Unexpected response code: 500"
692
693// IsRetryableError returns true for 500 errors from the Consul servers, and
694// network connection errors. These are usually retryable at a later time.
695// This applies to reads but NOT to writes. This may return true for errors
696// on writes that may have still gone through, so do not use this to retry
697// any write operations.
698func IsRetryableError(err error) bool {
699 if err == nil {
700 return false
701 }
702
703 if _, ok := err.(net.Error); ok {
704 return true
705 }
706
707 // TODO (slackpad) - Make a real error type here instead of using
708 // a string check.
709 return strings.Contains(err.Error(), serverError)
710}
711
712// setWriteOptions is used to annotate the request with
713// additional write options
714func (r *request) setWriteOptions(q *WriteOptions) {
715 if q == nil {
716 return
717 }
718 if q.Datacenter != "" {
719 r.params.Set("dc", q.Datacenter)
720 }
721 if q.Token != "" {
722 r.header.Set("X-Consul-Token", q.Token)
723 }
724 if q.RelayFactor != 0 {
725 r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
726 }
727 r.ctx = q.ctx
728}
729
730// toHTTP converts the request to an HTTP request
731func (r *request) toHTTP() (*http.Request, error) {
732 // Encode the query parameters
733 r.url.RawQuery = r.params.Encode()
734
735 // Check if we should encode the body
736 if r.body == nil && r.obj != nil {
737 b, err := encodeBody(r.obj)
738 if err != nil {
739 return nil, err
740 }
741 r.body = b
742 }
743
744 // Create the HTTP request
745 req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
746 if err != nil {
747 return nil, err
748 }
749
750 req.URL.Host = r.url.Host
751 req.URL.Scheme = r.url.Scheme
752 req.Host = r.url.Host
753 req.Header = r.header
754
755 // Setup auth
756 if r.config.HttpAuth != nil {
757 req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
758 }
759 if r.ctx != nil {
760 return req.WithContext(r.ctx), nil
761 }
762
763 return req, nil
764}
765
766// newRequest is used to create a new request
767func (c *Client) newRequest(method, path string) *request {
768 r := &request{
769 config: &c.config,
770 method: method,
771 url: &url.URL{
772 Scheme: c.config.Scheme,
773 Host: c.config.Address,
774 Path: path,
775 },
776 params: make(map[string][]string),
777 header: make(http.Header),
778 }
779 if c.config.Datacenter != "" {
780 r.params.Set("dc", c.config.Datacenter)
781 }
782 if c.config.WaitTime != 0 {
783 r.params.Set("wait", durToMsec(r.config.WaitTime))
784 }
785 if c.config.Token != "" {
786 r.header.Set("X-Consul-Token", r.config.Token)
787 }
788 return r
789}
790
791// doRequest runs a request with our client
792func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
793 req, err := r.toHTTP()
794 if err != nil {
795 return 0, nil, err
796 }
797 start := time.Now()
798 resp, err := c.config.HttpClient.Do(req)
799 diff := time.Since(start)
800 return diff, resp, err
801}
802
803// Query is used to do a GET request against an endpoint
804// and deserialize the response into an interface using
805// standard Consul conventions.
806func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
807 r := c.newRequest("GET", endpoint)
808 r.setQueryOptions(q)
809 rtt, resp, err := c.doRequest(r)
810 if err != nil {
811 return nil, err
812 }
813 defer resp.Body.Close()
814
815 qm := &QueryMeta{}
816 parseQueryMeta(resp, qm)
817 qm.RequestTime = rtt
818
819 if err := decodeBody(resp, out); err != nil {
820 return nil, err
821 }
822 return qm, nil
823}
824
825// write is used to do a PUT request against an endpoint
826// and serialize/deserialized using the standard Consul conventions.
827func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
828 r := c.newRequest("PUT", endpoint)
829 r.setWriteOptions(q)
830 r.obj = in
831 rtt, resp, err := requireOK(c.doRequest(r))
832 if err != nil {
833 return nil, err
834 }
835 defer resp.Body.Close()
836
837 wm := &WriteMeta{RequestTime: rtt}
838 if out != nil {
839 if err := decodeBody(resp, &out); err != nil {
840 return nil, err
841 }
842 } else if _, err := ioutil.ReadAll(resp.Body); err != nil {
843 return nil, err
844 }
845 return wm, nil
846}
847
848// parseQueryMeta is used to help parse query meta-data
Abhilash S.L3b494632019-07-16 15:51:09 +0530849//
850// TODO(rb): bug? the error from this function is never handled
William Kurkianea869482019-04-09 15:16:11 -0400851func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
852 header := resp.Header
853
854 // Parse the X-Consul-Index (if it's set - hash based blocking queries don't
855 // set this)
856 if indexStr := header.Get("X-Consul-Index"); indexStr != "" {
857 index, err := strconv.ParseUint(indexStr, 10, 64)
858 if err != nil {
859 return fmt.Errorf("Failed to parse X-Consul-Index: %v", err)
860 }
861 q.LastIndex = index
862 }
863 q.LastContentHash = header.Get("X-Consul-ContentHash")
864
865 // Parse the X-Consul-LastContact
866 last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64)
867 if err != nil {
868 return fmt.Errorf("Failed to parse X-Consul-LastContact: %v", err)
869 }
870 q.LastContact = time.Duration(last) * time.Millisecond
871
872 // Parse the X-Consul-KnownLeader
873 switch header.Get("X-Consul-KnownLeader") {
874 case "true":
875 q.KnownLeader = true
876 default:
877 q.KnownLeader = false
878 }
879
880 // Parse X-Consul-Translate-Addresses
881 switch header.Get("X-Consul-Translate-Addresses") {
882 case "true":
883 q.AddressTranslationEnabled = true
884 default:
885 q.AddressTranslationEnabled = false
886 }
887
888 // Parse Cache info
889 if cacheStr := header.Get("X-Cache"); cacheStr != "" {
890 q.CacheHit = strings.EqualFold(cacheStr, "HIT")
891 }
892 if ageStr := header.Get("Age"); ageStr != "" {
893 age, err := strconv.ParseUint(ageStr, 10, 64)
894 if err != nil {
895 return fmt.Errorf("Failed to parse Age Header: %v", err)
896 }
897 q.CacheAge = time.Duration(age) * time.Second
898 }
899
900 return nil
901}
902
903// decodeBody is used to JSON decode a body
904func decodeBody(resp *http.Response, out interface{}) error {
905 dec := json.NewDecoder(resp.Body)
906 return dec.Decode(out)
907}
908
909// encodeBody is used to encode a request body
910func encodeBody(obj interface{}) (io.Reader, error) {
911 buf := bytes.NewBuffer(nil)
912 enc := json.NewEncoder(buf)
913 if err := enc.Encode(obj); err != nil {
914 return nil, err
915 }
916 return buf, nil
917}
918
919// requireOK is used to wrap doRequest and check for a 200
920func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
921 if e != nil {
922 if resp != nil {
923 resp.Body.Close()
924 }
925 return d, nil, e
926 }
927 if resp.StatusCode != 200 {
Abhilash S.L3b494632019-07-16 15:51:09 +0530928 return d, nil, generateUnexpectedResponseCodeError(resp)
William Kurkianea869482019-04-09 15:16:11 -0400929 }
930 return d, resp, nil
931}
Abhilash S.L3b494632019-07-16 15:51:09 +0530932
933func (req *request) filterQuery(filter string) {
934 if filter == "" {
935 return
936 }
937
938 req.params.Set("filter", filter)
939}
940
941// generateUnexpectedResponseCodeError consumes the rest of the body, closes
942// the body stream and generates an error indicating the status code was
943// unexpected.
944func generateUnexpectedResponseCodeError(resp *http.Response) error {
945 var buf bytes.Buffer
946 io.Copy(&buf, resp.Body)
947 resp.Body.Close()
948 return fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
949}
950
951func requireNotFoundOrOK(d time.Duration, resp *http.Response, e error) (bool, time.Duration, *http.Response, error) {
952 if e != nil {
953 if resp != nil {
954 resp.Body.Close()
955 }
956 return false, d, nil, e
957 }
958 switch resp.StatusCode {
959 case 200:
960 return true, d, resp, nil
961 case 404:
962 return false, d, resp, nil
963 default:
964 return false, d, nil, generateUnexpectedResponseCodeError(resp)
965 }
966}