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