github.com/emate/nomad@v0.8.2-wo-binpacking/api/api.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "crypto/tls" 7 "encoding/json" 8 "fmt" 9 "io" 10 "net" 11 "net/http" 12 "net/url" 13 "os" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/hashicorp/go-cleanhttp" 19 rootcerts "github.com/hashicorp/go-rootcerts" 20 ) 21 22 var ( 23 // ClientConnTimeout is the timeout applied when attempting to contact a 24 // client directly before switching to a connection through the Nomad 25 // server. 26 ClientConnTimeout = 1 * time.Second 27 ) 28 29 // QueryOptions are used to parameterize a query 30 type QueryOptions struct { 31 // Providing a datacenter overwrites the region provided 32 // by the Config 33 Region string 34 35 // Namespace is the target namespace for the query. 36 Namespace string 37 38 // AllowStale allows any Nomad server (non-leader) to service 39 // a read. This allows for lower latency and higher throughput 40 AllowStale bool 41 42 // WaitIndex is used to enable a blocking query. Waits 43 // until the timeout or the next index is reached 44 WaitIndex uint64 45 46 // WaitTime is used to bound the duration of a wait. 47 // Defaults to that of the Config, but can be overridden. 48 WaitTime time.Duration 49 50 // If set, used as prefix for resource list searches 51 Prefix string 52 53 // Set HTTP parameters on the query. 54 Params map[string]string 55 56 // AuthToken is the secret ID of an ACL token 57 AuthToken string 58 } 59 60 // WriteOptions are used to parameterize a write 61 type WriteOptions struct { 62 // Providing a datacenter overwrites the region provided 63 // by the Config 64 Region string 65 66 // Namespace is the target namespace for the write. 67 Namespace string 68 69 // AuthToken is the secret ID of an ACL token 70 AuthToken string 71 } 72 73 // QueryMeta is used to return meta data about a query 74 type QueryMeta struct { 75 // LastIndex. This can be used as a WaitIndex to perform 76 // a blocking query 77 LastIndex uint64 78 79 // Time of last contact from the leader for the 80 // server servicing the request 81 LastContact time.Duration 82 83 // Is there a known leader 84 KnownLeader bool 85 86 // How long did the request take 87 RequestTime time.Duration 88 } 89 90 // WriteMeta is used to return meta data about a write 91 type WriteMeta struct { 92 // LastIndex. This can be used as a WaitIndex to perform 93 // a blocking query 94 LastIndex uint64 95 96 // How long did the request take 97 RequestTime time.Duration 98 } 99 100 // HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication 101 type HttpBasicAuth struct { 102 // Username to use for HTTP Basic Authentication 103 Username string 104 105 // Password to use for HTTP Basic Authentication 106 Password string 107 } 108 109 // Config is used to configure the creation of a client 110 type Config struct { 111 // Address is the address of the Nomad agent 112 Address string 113 114 // Region to use. If not provided, the default agent region is used. 115 Region string 116 117 // SecretID to use. This can be overwritten per request. 118 SecretID string 119 120 // Namespace to use. If not provided the default namespace is used. 121 Namespace string 122 123 // httpClient is the client to use. Default will be used if not provided. 124 httpClient *http.Client 125 126 // HttpAuth is the auth info to use for http access. 127 HttpAuth *HttpBasicAuth 128 129 // WaitTime limits how long a Watch will block. If not provided, 130 // the agent default values will be used. 131 WaitTime time.Duration 132 133 // TLSConfig provides the various TLS related configurations for the http 134 // client 135 TLSConfig *TLSConfig 136 } 137 138 // ClientConfig copies the configuration with a new client address, region, and 139 // whether the client has TLS enabled. 140 func (c *Config) ClientConfig(region, address string, tlsEnabled bool) *Config { 141 scheme := "http" 142 if tlsEnabled { 143 scheme = "https" 144 } 145 defaultConfig := DefaultConfig() 146 config := &Config{ 147 Address: fmt.Sprintf("%s://%s", scheme, address), 148 Region: region, 149 Namespace: c.Namespace, 150 httpClient: defaultConfig.httpClient, 151 SecretID: c.SecretID, 152 HttpAuth: c.HttpAuth, 153 WaitTime: c.WaitTime, 154 TLSConfig: c.TLSConfig.Copy(), 155 } 156 157 // Update the tls server name for connecting to a client 158 if tlsEnabled && config.TLSConfig != nil { 159 config.TLSConfig.TLSServerName = fmt.Sprintf("client.%s.nomad", region) 160 } 161 162 return config 163 } 164 165 // TLSConfig contains the parameters needed to configure TLS on the HTTP client 166 // used to communicate with Nomad. 167 type TLSConfig struct { 168 // CACert is the path to a PEM-encoded CA cert file to use to verify the 169 // Nomad server SSL certificate. 170 CACert string 171 172 // CAPath is the path to a directory of PEM-encoded CA cert files to verify 173 // the Nomad server SSL certificate. 174 CAPath string 175 176 // ClientCert is the path to the certificate for Nomad communication 177 ClientCert string 178 179 // ClientKey is the path to the private key for Nomad communication 180 ClientKey string 181 182 // TLSServerName, if set, is used to set the SNI host when connecting via 183 // TLS. 184 TLSServerName string 185 186 // Insecure enables or disables SSL verification 187 Insecure bool 188 } 189 190 func (t *TLSConfig) Copy() *TLSConfig { 191 if t == nil { 192 return nil 193 } 194 195 nt := new(TLSConfig) 196 *nt = *t 197 return nt 198 } 199 200 // DefaultConfig returns a default configuration for the client 201 func DefaultConfig() *Config { 202 config := &Config{ 203 Address: "http://127.0.0.1:4646", 204 httpClient: cleanhttp.DefaultClient(), 205 TLSConfig: &TLSConfig{}, 206 } 207 transport := config.httpClient.Transport.(*http.Transport) 208 transport.TLSHandshakeTimeout = 10 * time.Second 209 transport.TLSClientConfig = &tls.Config{ 210 MinVersion: tls.VersionTLS12, 211 } 212 213 if addr := os.Getenv("NOMAD_ADDR"); addr != "" { 214 config.Address = addr 215 } 216 if v := os.Getenv("NOMAD_REGION"); v != "" { 217 config.Region = v 218 } 219 if v := os.Getenv("NOMAD_NAMESPACE"); v != "" { 220 config.Namespace = v 221 } 222 if auth := os.Getenv("NOMAD_HTTP_AUTH"); auth != "" { 223 var username, password string 224 if strings.Contains(auth, ":") { 225 split := strings.SplitN(auth, ":", 2) 226 username = split[0] 227 password = split[1] 228 } else { 229 username = auth 230 } 231 232 config.HttpAuth = &HttpBasicAuth{ 233 Username: username, 234 Password: password, 235 } 236 } 237 238 // Read TLS specific env vars 239 if v := os.Getenv("NOMAD_CACERT"); v != "" { 240 config.TLSConfig.CACert = v 241 } 242 if v := os.Getenv("NOMAD_CAPATH"); v != "" { 243 config.TLSConfig.CAPath = v 244 } 245 if v := os.Getenv("NOMAD_CLIENT_CERT"); v != "" { 246 config.TLSConfig.ClientCert = v 247 } 248 if v := os.Getenv("NOMAD_CLIENT_KEY"); v != "" { 249 config.TLSConfig.ClientKey = v 250 } 251 if v := os.Getenv("NOMAD_SKIP_VERIFY"); v != "" { 252 if insecure, err := strconv.ParseBool(v); err == nil { 253 config.TLSConfig.Insecure = insecure 254 } 255 } 256 if v := os.Getenv("NOMAD_TOKEN"); v != "" { 257 config.SecretID = v 258 } 259 return config 260 } 261 262 // SetTimeout is used to place a timeout for connecting to Nomad. A negative 263 // duration is ignored, a duration of zero means no timeout, and any other value 264 // will add a timeout. 265 func (c *Config) SetTimeout(t time.Duration) error { 266 if c == nil { 267 return fmt.Errorf("nil config") 268 } else if c.httpClient == nil { 269 return fmt.Errorf("nil HTTP client") 270 } else if c.httpClient.Transport == nil { 271 return fmt.Errorf("nil HTTP client transport") 272 } 273 274 // Apply a timeout. 275 if t.Nanoseconds() >= 0 { 276 transport, ok := c.httpClient.Transport.(*http.Transport) 277 if !ok { 278 return fmt.Errorf("unexpected HTTP transport: %T", c.httpClient.Transport) 279 } 280 281 transport.DialContext = (&net.Dialer{ 282 Timeout: t, 283 KeepAlive: 30 * time.Second, 284 }).DialContext 285 } 286 287 return nil 288 } 289 290 // ConfigureTLS applies a set of TLS configurations to the the HTTP client. 291 func (c *Config) ConfigureTLS() error { 292 if c.TLSConfig == nil { 293 return nil 294 } 295 if c.httpClient == nil { 296 return fmt.Errorf("config HTTP Client must be set") 297 } 298 299 var clientCert tls.Certificate 300 foundClientCert := false 301 if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" { 302 if c.TLSConfig.ClientCert != "" && c.TLSConfig.ClientKey != "" { 303 var err error 304 clientCert, err = tls.LoadX509KeyPair(c.TLSConfig.ClientCert, c.TLSConfig.ClientKey) 305 if err != nil { 306 return err 307 } 308 foundClientCert = true 309 } else { 310 return fmt.Errorf("Both client cert and client key must be provided") 311 } 312 } 313 314 clientTLSConfig := c.httpClient.Transport.(*http.Transport).TLSClientConfig 315 rootConfig := &rootcerts.Config{ 316 CAFile: c.TLSConfig.CACert, 317 CAPath: c.TLSConfig.CAPath, 318 } 319 if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil { 320 return err 321 } 322 323 clientTLSConfig.InsecureSkipVerify = c.TLSConfig.Insecure 324 325 if foundClientCert { 326 clientTLSConfig.Certificates = []tls.Certificate{clientCert} 327 } 328 if c.TLSConfig.TLSServerName != "" { 329 clientTLSConfig.ServerName = c.TLSConfig.TLSServerName 330 } 331 332 return nil 333 } 334 335 // Client provides a client to the Nomad API 336 type Client struct { 337 config Config 338 } 339 340 // NewClient returns a new client 341 func NewClient(config *Config) (*Client, error) { 342 // bootstrap the config 343 defConfig := DefaultConfig() 344 345 if config.Address == "" { 346 config.Address = defConfig.Address 347 } else if _, err := url.Parse(config.Address); err != nil { 348 return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err) 349 } 350 351 if config.httpClient == nil { 352 config.httpClient = defConfig.httpClient 353 } 354 355 // Configure the TLS configurations 356 if err := config.ConfigureTLS(); err != nil { 357 return nil, err 358 } 359 360 client := &Client{ 361 config: *config, 362 } 363 return client, nil 364 } 365 366 // Address return the address of the Nomad agent 367 func (c *Client) Address() string { 368 return c.config.Address 369 } 370 371 // SetRegion sets the region to forward API requests to. 372 func (c *Client) SetRegion(region string) { 373 c.config.Region = region 374 } 375 376 // SetNamespace sets the namespace to forward API requests to. 377 func (c *Client) SetNamespace(namespace string) { 378 c.config.Namespace = namespace 379 } 380 381 // GetNodeClient returns a new Client that will dial the specified node. If the 382 // QueryOptions is set, its region will be used. 383 func (c *Client) GetNodeClient(nodeID string, q *QueryOptions) (*Client, error) { 384 return c.getNodeClientImpl(nodeID, -1, q, c.Nodes().Info) 385 } 386 387 // GetNodeClientWithTimeout returns a new Client that will dial the specified 388 // node using the specified timeout. If the QueryOptions is set, its region will 389 // be used. 390 func (c *Client) GetNodeClientWithTimeout( 391 nodeID string, timeout time.Duration, q *QueryOptions) (*Client, error) { 392 return c.getNodeClientImpl(nodeID, timeout, q, c.Nodes().Info) 393 } 394 395 // nodeLookup is the definition of a function used to lookup a node. This is 396 // largely used to mock the lookup in tests. 397 type nodeLookup func(nodeID string, q *QueryOptions) (*Node, *QueryMeta, error) 398 399 // getNodeClientImpl is the implementation of creating a API client for 400 // contacting a node. It takes a function to lookup the node such that it can be 401 // mocked during tests. 402 func (c *Client) getNodeClientImpl(nodeID string, timeout time.Duration, q *QueryOptions, lookup nodeLookup) (*Client, error) { 403 node, _, err := lookup(nodeID, q) 404 if err != nil { 405 return nil, err 406 } 407 if node.Status == "down" { 408 return nil, NodeDownErr 409 } 410 if node.HTTPAddr == "" { 411 return nil, fmt.Errorf("http addr of node %q (%s) is not advertised", node.Name, nodeID) 412 } 413 414 var region string 415 switch { 416 case q != nil && q.Region != "": 417 // Prefer the region set in the query parameter 418 region = q.Region 419 case c.config.Region != "": 420 // If the client is configured for a particular region use that 421 region = c.config.Region 422 default: 423 // No region information is given so use the default. 424 region = "global" 425 } 426 427 // Get an API client for the node 428 conf := c.config.ClientConfig(region, node.HTTPAddr, node.TLSEnabled) 429 430 // Set the timeout 431 conf.SetTimeout(timeout) 432 433 return NewClient(conf) 434 } 435 436 // SetSecretID sets the ACL token secret for API requests. 437 func (c *Client) SetSecretID(secretID string) { 438 c.config.SecretID = secretID 439 } 440 441 // request is used to help build up a request 442 type request struct { 443 config *Config 444 method string 445 url *url.URL 446 params url.Values 447 token string 448 body io.Reader 449 obj interface{} 450 } 451 452 // setQueryOptions is used to annotate the request with 453 // additional query options 454 func (r *request) setQueryOptions(q *QueryOptions) { 455 if q == nil { 456 return 457 } 458 if q.Region != "" { 459 r.params.Set("region", q.Region) 460 } 461 if q.Namespace != "" { 462 r.params.Set("namespace", q.Namespace) 463 } 464 if q.AuthToken != "" { 465 r.token = q.AuthToken 466 } 467 if q.AllowStale { 468 r.params.Set("stale", "") 469 } 470 if q.WaitIndex != 0 { 471 r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) 472 } 473 if q.WaitTime != 0 { 474 r.params.Set("wait", durToMsec(q.WaitTime)) 475 } 476 if q.Prefix != "" { 477 r.params.Set("prefix", q.Prefix) 478 } 479 for k, v := range q.Params { 480 r.params.Set(k, v) 481 } 482 } 483 484 // durToMsec converts a duration to a millisecond specified string 485 func durToMsec(dur time.Duration) string { 486 return fmt.Sprintf("%dms", dur/time.Millisecond) 487 } 488 489 // setWriteOptions is used to annotate the request with 490 // additional write options 491 func (r *request) setWriteOptions(q *WriteOptions) { 492 if q == nil { 493 return 494 } 495 if q.Region != "" { 496 r.params.Set("region", q.Region) 497 } 498 if q.Namespace != "" { 499 r.params.Set("namespace", q.Namespace) 500 } 501 if q.AuthToken != "" { 502 r.token = q.AuthToken 503 } 504 } 505 506 // toHTTP converts the request to an HTTP request 507 func (r *request) toHTTP() (*http.Request, error) { 508 // Encode the query parameters 509 r.url.RawQuery = r.params.Encode() 510 511 // Check if we should encode the body 512 if r.body == nil && r.obj != nil { 513 if b, err := encodeBody(r.obj); err != nil { 514 return nil, err 515 } else { 516 r.body = b 517 } 518 } 519 520 // Create the HTTP request 521 req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) 522 if err != nil { 523 return nil, err 524 } 525 526 // Optionally configure HTTP basic authentication 527 if r.url.User != nil { 528 username := r.url.User.Username() 529 password, _ := r.url.User.Password() 530 req.SetBasicAuth(username, password) 531 } else if r.config.HttpAuth != nil { 532 req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) 533 } 534 535 req.Header.Add("Accept-Encoding", "gzip") 536 if r.token != "" { 537 req.Header.Set("X-Nomad-Token", r.token) 538 } 539 540 req.URL.Host = r.url.Host 541 req.URL.Scheme = r.url.Scheme 542 req.Host = r.url.Host 543 return req, nil 544 } 545 546 // newRequest is used to create a new request 547 func (c *Client) newRequest(method, path string) (*request, error) { 548 base, _ := url.Parse(c.config.Address) 549 u, err := url.Parse(path) 550 if err != nil { 551 return nil, err 552 } 553 r := &request{ 554 config: &c.config, 555 method: method, 556 url: &url.URL{ 557 Scheme: base.Scheme, 558 User: base.User, 559 Host: base.Host, 560 Path: u.Path, 561 }, 562 params: make(map[string][]string), 563 } 564 if c.config.Region != "" { 565 r.params.Set("region", c.config.Region) 566 } 567 if c.config.Namespace != "" { 568 r.params.Set("namespace", c.config.Namespace) 569 } 570 if c.config.WaitTime != 0 { 571 r.params.Set("wait", durToMsec(r.config.WaitTime)) 572 } 573 if c.config.SecretID != "" { 574 r.token = r.config.SecretID 575 } 576 577 // Add in the query parameters, if any 578 for key, values := range u.Query() { 579 for _, value := range values { 580 r.params.Add(key, value) 581 } 582 } 583 584 return r, nil 585 } 586 587 // multiCloser is to wrap a ReadCloser such that when close is called, multiple 588 // Closes occur. 589 type multiCloser struct { 590 reader io.Reader 591 inorderClose []io.Closer 592 } 593 594 func (m *multiCloser) Close() error { 595 for _, c := range m.inorderClose { 596 if err := c.Close(); err != nil { 597 return err 598 } 599 } 600 return nil 601 } 602 603 func (m *multiCloser) Read(p []byte) (int, error) { 604 return m.reader.Read(p) 605 } 606 607 // doRequest runs a request with our client 608 func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { 609 req, err := r.toHTTP() 610 if err != nil { 611 return 0, nil, err 612 } 613 start := time.Now() 614 resp, err := c.config.httpClient.Do(req) 615 diff := time.Now().Sub(start) 616 617 // If the response is compressed, we swap the body's reader. 618 if resp != nil && resp.Header != nil { 619 var reader io.ReadCloser 620 switch resp.Header.Get("Content-Encoding") { 621 case "gzip": 622 greader, err := gzip.NewReader(resp.Body) 623 if err != nil { 624 return 0, nil, err 625 } 626 627 // The gzip reader doesn't close the wrapped reader so we use 628 // multiCloser. 629 reader = &multiCloser{ 630 reader: greader, 631 inorderClose: []io.Closer{greader, resp.Body}, 632 } 633 default: 634 reader = resp.Body 635 } 636 resp.Body = reader 637 } 638 639 return diff, resp, err 640 } 641 642 // rawQuery makes a GET request to the specified endpoint but returns just the 643 // response body. 644 func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) { 645 r, err := c.newRequest("GET", endpoint) 646 if err != nil { 647 return nil, err 648 } 649 r.setQueryOptions(q) 650 _, resp, err := requireOK(c.doRequest(r)) 651 if err != nil { 652 return nil, err 653 } 654 655 return resp.Body, nil 656 } 657 658 // query is used to do a GET request against an endpoint 659 // and deserialize the response into an interface using 660 // standard Nomad conventions. 661 func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) { 662 r, err := c.newRequest("GET", endpoint) 663 if err != nil { 664 return nil, err 665 } 666 r.setQueryOptions(q) 667 rtt, resp, err := requireOK(c.doRequest(r)) 668 if err != nil { 669 return nil, err 670 } 671 defer resp.Body.Close() 672 673 qm := &QueryMeta{} 674 parseQueryMeta(resp, qm) 675 qm.RequestTime = rtt 676 677 if err := decodeBody(resp, out); err != nil { 678 return nil, err 679 } 680 return qm, nil 681 } 682 683 // putQuery is used to do a PUT request when doing a read against an endpoint 684 // and deserialize the response into an interface using standard Nomad 685 // conventions. 686 func (c *Client) putQuery(endpoint string, in, out interface{}, q *QueryOptions) (*QueryMeta, error) { 687 r, err := c.newRequest("PUT", endpoint) 688 if err != nil { 689 return nil, err 690 } 691 r.setQueryOptions(q) 692 r.obj = in 693 rtt, resp, err := requireOK(c.doRequest(r)) 694 if err != nil { 695 return nil, err 696 } 697 defer resp.Body.Close() 698 699 qm := &QueryMeta{} 700 parseQueryMeta(resp, qm) 701 qm.RequestTime = rtt 702 703 if err := decodeBody(resp, out); err != nil { 704 return nil, err 705 } 706 return qm, nil 707 } 708 709 // write is used to do a PUT request against an endpoint 710 // and serialize/deserialized using the standard Nomad conventions. 711 func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) { 712 r, err := c.newRequest("PUT", endpoint) 713 if err != nil { 714 return nil, err 715 } 716 r.setWriteOptions(q) 717 r.obj = in 718 rtt, resp, err := requireOK(c.doRequest(r)) 719 if err != nil { 720 return nil, err 721 } 722 defer resp.Body.Close() 723 724 wm := &WriteMeta{RequestTime: rtt} 725 parseWriteMeta(resp, wm) 726 727 if out != nil { 728 if err := decodeBody(resp, &out); err != nil { 729 return nil, err 730 } 731 } 732 return wm, nil 733 } 734 735 // delete is used to do a DELETE request against an endpoint 736 // and serialize/deserialized using the standard Nomad conventions. 737 func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) { 738 r, err := c.newRequest("DELETE", endpoint) 739 if err != nil { 740 return nil, err 741 } 742 r.setWriteOptions(q) 743 rtt, resp, err := requireOK(c.doRequest(r)) 744 if err != nil { 745 return nil, err 746 } 747 defer resp.Body.Close() 748 749 wm := &WriteMeta{RequestTime: rtt} 750 parseWriteMeta(resp, wm) 751 752 if out != nil { 753 if err := decodeBody(resp, &out); err != nil { 754 return nil, err 755 } 756 } 757 return wm, nil 758 } 759 760 // parseQueryMeta is used to help parse query meta-data 761 func parseQueryMeta(resp *http.Response, q *QueryMeta) error { 762 header := resp.Header 763 764 // Parse the X-Nomad-Index 765 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 766 if err != nil { 767 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 768 } 769 q.LastIndex = index 770 771 // Parse the X-Nomad-LastContact 772 last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64) 773 if err != nil { 774 return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err) 775 } 776 q.LastContact = time.Duration(last) * time.Millisecond 777 778 // Parse the X-Nomad-KnownLeader 779 switch header.Get("X-Nomad-KnownLeader") { 780 case "true": 781 q.KnownLeader = true 782 default: 783 q.KnownLeader = false 784 } 785 return nil 786 } 787 788 // parseWriteMeta is used to help parse write meta-data 789 func parseWriteMeta(resp *http.Response, q *WriteMeta) error { 790 header := resp.Header 791 792 // Parse the X-Nomad-Index 793 index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64) 794 if err != nil { 795 return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err) 796 } 797 q.LastIndex = index 798 return nil 799 } 800 801 // decodeBody is used to JSON decode a body 802 func decodeBody(resp *http.Response, out interface{}) error { 803 dec := json.NewDecoder(resp.Body) 804 return dec.Decode(out) 805 } 806 807 // encodeBody is used to encode a request body 808 func encodeBody(obj interface{}) (io.Reader, error) { 809 buf := bytes.NewBuffer(nil) 810 enc := json.NewEncoder(buf) 811 if err := enc.Encode(obj); err != nil { 812 return nil, err 813 } 814 return buf, nil 815 } 816 817 // requireOK is used to wrap doRequest and check for a 200 818 func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { 819 if e != nil { 820 if resp != nil { 821 resp.Body.Close() 822 } 823 return d, nil, e 824 } 825 if resp.StatusCode != 200 { 826 var buf bytes.Buffer 827 io.Copy(&buf, resp.Body) 828 resp.Body.Close() 829 return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) 830 } 831 return d, resp, nil 832 }