github.com/criteo-forks/consul@v1.4.5-criteonogrpc/api/api.go (about)

     1  package api
     2  
     3  import (
     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  
    24  const (
    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
    73  type 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  
   151  func (o *QueryOptions) Context() context.Context {
   152  	if o != nil && o.ctx != nil {
   153  		return o.ctx
   154  	}
   155  	return context.Background()
   156  }
   157  
   158  func (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
   168  type 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  
   187  func (o *WriteOptions) Context() context.Context {
   188  	if o != nil && o.ctx != nil {
   189  		return o.ctx
   190  	}
   191  	return context.Background()
   192  }
   193  
   194  func (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
   204  type 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
   236  type 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
   242  type 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
   251  type 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.
   284  type 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.
   316  func 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.
   325  func 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.
   331  func 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.
   403  func 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  
   442  func (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
   466  type Client struct {
   467  	config Config
   468  }
   469  
   470  // NewClient returns a new client
   471  func 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.
   549  func 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
   576  type 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
   589  func (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.
   649  func 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.
   658  const 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.
   665  func 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
   681  func (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
   698  func (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
   734  func (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
   759  func (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.
   773  func (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.
   794  func (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
   816  func 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
   869  func 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
   875  func 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
   885  func 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  }