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