github.com/DerekStrickland/consul@v1.4.5/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 }