1package api 2 3import ( 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 cleanhttp "github.com/hashicorp/go-cleanhttp" 19 rootcerts "github.com/hashicorp/go-rootcerts" 20) 21 22var ( 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 parametrize a query 30type 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 parametrize a write 61type 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 74type 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 91type 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 101type 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 110type 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. 140func (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. 167type 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 190func (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 201func 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. 265func (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. 291func (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 336type Client struct { 337 config Config 338} 339 340// NewClient returns a new client 341func 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 367func (c *Client) Address() string { 368 return c.config.Address 369} 370 371// SetRegion sets the region to forward API requests to. 372func (c *Client) SetRegion(region string) { 373 c.config.Region = region 374} 375 376// SetNamespace sets the namespace to forward API requests to. 377func (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. 383func (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. 390func (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. 397type 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. 402func (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. 437func (c *Client) SetSecretID(secretID string) { 438 c.config.SecretID = secretID 439} 440 441// request is used to help build up a request 442type 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 454func (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 485func 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 491func (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 507func (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 547func (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. 589type multiCloser struct { 590 reader io.Reader 591 inorderClose []io.Closer 592} 593 594func (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 603func (m *multiCloser) Read(p []byte) (int, error) { 604 return m.reader.Read(p) 605} 606 607// doRequest runs a request with our client 608func (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. 644func (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. 661func (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. 686func (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. 711func (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. 737func (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 761func 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 789func 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 802func 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 808func 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 818func 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} 833